Skip to main content

TypeScript Declaration Merging

Declaration merging is what TypeScript does when it finds several declarations with the same name: it combines them into one definition.


Understanding Declaration Merging

Declaration merging lets you combine multiple declarations that share a name into a single definition. This lets you build up complex types step by step and extend existing types without losing type safety.

Key benefits:

  • Progressive enhancement: build a type incrementally across several declarations.
  • Extensibility: add members to an existing type without touching its original definition.
  • Organization: split a large type into logical groups.
  • Compatibility: extend third-party type definitions when you need to.

Common use cases:

  • Extending built-in types and third-party library types.
  • Adding type information for JavaScript libraries.
  • Organizing large interfaces across multiple files.
  • Creating fluent APIs with method chaining.
  • Implementing the module augmentation pattern.

Interface Merging

Interfaces with the same name are merged automatically. The result has every member from every declaration.

Example

The two Person declarations become one interface with name, age, address and email, so an object must provide all four.

Prompt
Show that two interface Person declarations in the same file are merged into one type. The first declares name: string and age: number, the second declares address: string and email: string. Create a person object with all four properties (John, 30, "123 Main St", [email protected]) and log the whole object.
Output: TypeScript file and captured output

Function Overloads with Merging

You can write several function signatures with the same name. They merge into one overloaded function, and a single implementation handles every case.

Example

processValue has three overload signatures and one implementation. The overloads tell callers exactly which return type matches each argument type.

Prompt
Declare three overload signatures for processValue — one taking and returning string, one number, one boolean — followed by a single implementation that accepts string | number | boolean. The implementation upper-cases strings, doubles numbers and negates booleans. Call it with "hello", 10 and true and log each result.
Output: TypeScript file and captured output

Namespace Merging

Namespaces with the same name are merged too. Exported members from one block are visible in the others.

Example

The second Validation block can implement StringValidator from the first block, and code outside reaches everything through the single merged namespace.

Prompt
Show two namespace Validation blocks merging into one namespace. The first exports a StringValidator interface with isValid(s: string): boolean; the second exports a NumberValidator interface and a ZipCodeValidator class that implements StringValidator by checking for exactly five digits. Create a ZipCodeValidator through Validation.ZipCodeValidator and log isValid for "12345", "1234" and "abcde".
Output: TypeScript file and captured output

Class and Interface Merging

A class declaration can merge with an interface of the same name. The interface adds members to the class type, and the class must implement them.

Merging a class with an interface

The Cart interface declares calculateTotal(), and the Cart class provides it along with its own items and addItem members.

script.ts
// Interface declaration
interface Cart {
  calculateTotal(): number;
}

// Class declaration with same name
class Cart {
  items: { name: string; price: number }[] = [];

  addItem(name: string, price: number): void {
    this.items.push({ name, price });
  }

    // Must implement the interface method
    calculateTotal(): number {
    return this.items.reduce((sum, item) => sum + item.price, 0);
  }
}

// Using the merged class and interface
const cart = new Cart();
cart.addItem("Book", 15.99);
cart.addItem("Coffee Mug", 8.99);

console.log(`Total: $${cart.calculateTotal().toFixed(2)}`);

Enum Merging

Enum declarations with the same name are merged as well. Only the first member of the first declaration may leave out its value; later declarations must set explicit values.

Example

Direction ends up with four members. Reverse lookup by value (Direction[0]) works across the merged enum.

Prompt
Show two enum Direction declarations merging into one enum. The first declares North and South (auto-numbered 0 and 1); the second declares East = 2 and West = 3. Log the numeric value of all four members, then log Direction[0] and Direction[2] to show reverse lookup by value.
Output: TypeScript file and captured output

Module Augmentation

You can extend an existing module or library by declaring additional types and functions with the same name. This is called module augmentation.

Example

The first declare namespace LibraryModule stands in for a third-party library. The second block adds a new interface, a new optional property on User and a new function. Because declare blocks contain no runtime code, the example attaches a tiny stand-in implementation to globalThis so it can run.

Prompt
Show module augmentation with two declare namespace LibraryModule blocks: the first (standing in for a third-party library) declares a User interface (id, name) and a getUser(id: number): User function; the second adds a UserPreferences interface (theme, notifications), adds an optional preferences?: UserPreferences to User, and declares getUserPreferences(userId: number): UserPreferences. Because declare blocks have no runtime code, attach a small stand-in implementation of LibraryModule to globalThis so the file can run. Call getUser(123) and log user.preferences?.theme, then call getUserPreferences(123) and log prefs.notifications.
Output: TypeScript file and captured output

Best Practices

There are some rules to keep in mind when using declaration merging:

  • Order matters for function overloads: the implementation signature should be the most general one.
  • Non-function members must be compatible: if two interfaces declare a property with the same name, the types must be identical or compatible.
  • Later interfaces take precedence: if merged interfaces conflict, the last declaration wins.
  • Private and protected members: classes cannot merge if they have private or protected members with the same name but different types.
  • Namespace exports: only exported declarations are visible outside the namespace after merging.

Performance Considerations

  • Compilation time: heavy use of declaration merging can slow down compilation.
  • Type checking: complex merged types may slow down your editor.
  • Bundle size: declaration merging has no effect on runtime performance or bundle size.

Optimization tips:

  • Keep merged interfaces focused and cohesive.
  • Avoid deep nesting in merged types.
  • Use type aliases for simple type combinations instead of merging.