Ch. 2 · TypeScript

TypeScript Declaration Merging and Module Augmentation

Merge interfaces and namespaces, augment existing modules and globals safely, and avoid the conflicts that type aliases cannot resolve.

~3 min readadvancedupdated Oct 4, 2026

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 {};
TypeScript

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.

More in TypeScript

read ✓TypeScript · hard

TypeScript Abstract Classes and Contracts

Share behavior with abstract classes, enforce required members, and decide when an interface or composition is the better contract.

~2 min readread →
read ✓TypeScript · mid

TypeScript Enums vs Union Types

Compare enums with unions of string literals: runtime cost, serialization, exhaustiveness and which one fits application code.

~3 min readread →
esc