TypeScript can combine declarations that share a name. Two interface declarations with the same name merge into one, namespaces merge too, and declare module / declare global add members to an existing module or the global scope. This is how libraries let consumers add fields to their types, but it is also a source of surprising global changes.
Before you start
You should understand modules, interface versus type, and ambient declarations. This article is about merging rules and safe augmentation; it does not cover writing a full .d.ts for an untyped package.
Step-by-step walkthrough
Step 1: Merge interfaces, not type aliases
Two interfaces with the same name in the same scope merge their members; a duplicate property of a conflicting type is an error, and a compatible duplicate is allowed. A type alias cannot merge: declaring it twice is an immediate “duplicate identifier” error. That asymmetry is a common interview question and a design lever.
Step 2: Augment without changing identity
declare module './events' { interface EventMap { ... } } reopens an existing module and adds to an interface it exports. The augmentation must be inside a module (a file with imports or export {}), so it is scoped rather than silent. Merging changes the type globally for every importer, so add optional members and keep the base interface the source of truth.
Step 3: Extend globals explicitly
declare global adds to the global scope, such as a Window property. Because it affects every file, guard it behind a small, clearly named module and document the runtime setup that must accompany the type. A type-only global that no code initializes is a promise the type cannot keep.
Worked scenario
Two interfaces merge into one; a module augmentation adds an optional global field.
interface Session {
id: string;
}
interface Session {
userId: string;
}
const session: Session = { id: 's1', userId: 'u1' };
declare global {
interface Window {
analytics?: { track(event: string): void };
}
}
export {};Walk through the example
The two Session declarations combine, so session must provide both id and userId. The declare global block adds an optional analytics field to Window, so window.analytics?.track('open') type-checks while unsupported environments remain valid. export {} makes the file a module, which is required for declare global to work.
Common mistake
Trying to merge type aliases and expecting it to compile, or augmenting a module from a script file (no imports or exports) so the augmentation leaks into the global scope instead. Another trap is making an augmented member required: existing code that does not set it now fails, even though the runtime never guaranteed it.
Verify the behavior
Define an object typed with the merged interface and confirm it needs every member, then remove one and check the error. Add a conflicting property type in a second interface declaration and confirm the compiler rejects the merge. For the global augmentation, import something in another file and confirm window.analytics is visible there and optional, so code must still null-check it.
Interview exercise
A design system wants consumers to add custom theme colors without forking its types. How should it expose that, and what rule keeps it safe?
Answer and reasoning
Export an interface such as ThemeColors and document that consumers may reopen it with declare module 'design-system' { interface ThemeColors { brand: string } }. Keeping it an interface makes merging possible, and documenting the augmentation keeps the change local to the consumer. The augmentation should add optional members unless the consumer also updates every construction site, so the base library and its existing users keep compiling.
Follow-up discussion
Do merged interfaces affect bundle size? No: augmentation is type-level only and disappears at compile time. Can you remove a merged member later? Not cleanly: once consumers depend on an augmented member, removing the declaration breaks their build, so treat augmentation as an additive, versioned contract.
Continue learning
Compare the trade-offs in type versus interface and module type imports. Read the TypeScript declaration merging handbook and try the TypeScript MCQs.