TypeScript Declaration Merging
Understanding Declaration Merging
Declaration merging is a powerful TypeScript feature that allows you to combine multiple declarations with the same name into a single definition.
This enables you to build up complex types incrementally and extend existing types in a type-safe manner.
Key Benefits
- Progressive Enhancement: Build types incrementally across multiple declarations
- Extensibility: Add new members to existing types without modifying original definitions
- Organization: Split large type definitions into logical groupings
- Compatibility: Extend third-party type definitions when needed
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 automatically merged:
// First declaration
interface Person {
name: string;
age: number;
}
// Second declaration with the same name
interface Person {
address: string;
email: string;
}
// TypeScript merges them into:
// interface Person {
// name: string;
// age: number;
// address: string;
// email: string;
// }
const person: Person = {
name: "John",
age: 30,
address: "123 Main St",
email: "[email protected]"
};
console.log(person);
Try it Yourself »
Function Overloads with Merging
You can define multiple function declarations that later merge when implemented:
// Function overloads
function processValue(value: string): string;
function processValue(value: number): number;
function processValue(value: boolean): boolean;
// Implementation that handles all overloads
function processValue(value: string | number | boolean): string | number | boolean {
if (typeof value === "string") {
return value.toUpperCase();
} else if (typeof value === "number") {
return value * 2;
} else {
return !value;
}
}
// Using the function with different types
console.log(processValue("hello")); // "HELLO"
console.log(processValue(10)); // 20
console.log(processValue(true)); // false
Try it Yourself »
Namespace Merging
Namespaces with the same name are merged:
namespace Validation {
export interface StringValidator {
isValid(s: string): boolean;
}
}
namespace Validation {
export interface NumberValidator {
isValid(n: number): boolean;
}
export class ZipCodeValidator implements StringValidator {
isValid(s: string): boolean {
return s.length === 5 && /^\d+$/.test(s);
}
}
}
// After merging:
// namespace Validation {
// export interface StringValidator { isValid(s: string): boolean; }
// export interface NumberValidator { isValid(n: number): boolean; }
// export class ZipCodeValidator implements StringValidator { ... }
// }
// Using the merged namespace
const zipValidator = new Validation.ZipCodeValidator();
console.log(zipValidator.isValid("12345")); // true
console.log(zipValidator.isValid("1234")); // false
console.log(zipValidator.isValid("abcde")); // false
Try it Yourself »
Class and Interface Merging
A class declaration can merge with an interface of the same name:
// 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:
// First part of the enum
enum Direction {
North,
South
}
// Second part of the enum
enum Direction {
East = 2,
West = 3
}
// After merging:
// enum Direction {
// North = 0,
// South = 1,
// East = 2,
// West = 3
// }
console.log(Direction.North); // 0
console.log(Direction.South); // 1
console.log(Direction.East); // 2
console.log(Direction.West); // 3
// Can also access by value
console.log(Direction[0]); // "North"
console.log(Direction[2]); // "East"
Try it Yourself »
Module Augmentation
You can extend existing modules or libraries by declaring additional types and functionality:
// Original library definition
// Imagine this comes from a third-party library
declare namespace LibraryModule {
export interface User {
id: number;
name: string;
}
export function getUser(id: number): User;
}
// Augmenting with additional functionality (your code)
declare namespace LibraryModule {
// Add new interface
export interface UserPreferences {
theme: string;
notifications: boolean;
}
// Add new property to existing interface
export interface User {
preferences?: UserPreferences;
}
// Add new function
export function getUserPreferences(userId: number): UserPreferences;
}
// Using the augmented module
const user = LibraryModule.getUser(123);
console.log(user.preferences?.theme);
const prefs = LibraryModule.getUserPreferences(123);
console.log(prefs.notifications);
Try it Yourself »
Best Practices
There are some rules to consider when using declaration merging:
- Order matters for function overloads: The implementation signature should be the most general
- 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 conflicts exist in merged interfaces, the last declaration wins
- Private and protected members: Classes can't 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: Excessive declaration merging can increase compilation time
- Type Checking: Complex merged types may impact IDE performance
- Bundle Size: Declaration merging doesn't affect 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