Lesson 59 of 80
declare module gets you started, but a real untyped package's actual runtime API — a callable default export with methods attached, optional config objects — needs a shape most people don't know TypeScript can even describe.
15 min read
Every option explains itself — why the right one is right, and why each wrong one is wrong. Miss one and it comes back later on a spaced-repetition schedule.
This topic is about judgment and practical tradeoffs more than a single right answer — a mock interview is the better way to test yourself on it.
Be interviewed on this →Hints without spoilers, and a plain explanation of why a wrong answer was wrong — answered only from this lesson. Free account, no card.
Sign up freeWhat interviewers actually ask about this, and what a strong answer has to cover.
A strong answer covers
Likely follow-ups
The declare module mechanic itself is simple, and covered elsewhere in this track: it tells TypeScript "trust me, this module exists with this shape." What's harder — and what actually happens when you reach for this in a real codebase — is matching that declaration to a real package's actual runtime API, which is rarely just "one function." Default exports that are callable and have methods attached, options objects with every field optional, named exports alongside a default — a real .d.ts has to model whatever shape the package genuinely has, not the simplest shape you can imagine.
Say tiny-confetti is a small untyped npm package (no bundled types, no @types/tiny-confetti published) whose actual runtime API, from reading its README and source, looks like this:
import confetti from "tiny-confetti";
confetti(); // fire with defaults
confetti({ particleCount: 200, spread: 70 }); // fire with options, returns a Promise
confetti.reset(); // clear any running animation
The default export is a function and has a reset method hanging off it — a shape plain function types can't express on their own:
// tiny-confetti.d.ts
declare module "tiny-confetti" {
interface ConfettiOptions {
particleCount?: number;
spread?: number;
origin?: { x: number; y: number };
}
interface ConfettiFn {
(options?: ConfettiOptions): Promise<void>; // call signature — makes it callable
reset(): void; // ordinary method on the same object
}
const confetti: ConfettiFn;
export default confetti;
}
ConfettiFn is an interface with a call signature — the (options?: ConfettiOptions): Promise<void> line with no name — plus a regular method. That combination is exactly how TypeScript describes "a callable thing that also has properties," which is what a function-with-attached-methods actually is at runtime. Import confetti from "tiny-confetti" anywhere in the project now, and both confetti({ spread: 90 }) and confetti.reset() type-check against this one declaration.
(x: number) => string) and reach for an awkward two-declaration workaround instead of the call-signature-in-an-interface pattern shown above.declare module "tiny-confetti" only attaches to import ... from "tiny-confetti" — a typo in that string, or matching the wrong subpath, silently fails to attach, with no warning either way..d.ts becomes a liability, not a convenience, once the real package ships its own types (or an official @types/ package appears) — it now conflicts with or shadows the real one and needs deleting, not just ignoring, or you're maintaining a drifted-from-reality copy indefinitely.tiny-confetti's real implementation matches what the .d.ts claims — get a parameter's optionality or a return type wrong, and TypeScript will confidently, incorrectly, tell callers it's safe.declare mechanic, applied to a harder real shapeThe ambient-declarations lesson introduced declare and declare module through a single-function toy example — enough to show the mechanic, not enough to prepare you for what a real package usually looks like. This lesson is that same declare module syntax, aimed at the actual shape untyped packages tend to have: a callable default export carrying its own methods, which needs the call-signature-plus-method pattern this lesson introduces on top of the earlier one.