bugl
bugl
HomeLearnPatternsPathsSearchPremium
HomeLearnPatternsPaths
Learn/TypeScript/TypeScript Core
TypeScript•TypeScript Core

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:

Example

// 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: "john@example.com"
};
console.log(person);

Function Overloads with Merging

You can define multiple function declarations that later merge when implemented:

Example

// 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

Namespace Merging

Namespaces with the same name are merged:

Example

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

Class and Interface Merging

A class declaration can merge with an interface of the same name:

Example

// 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:

Example

// 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"

Module Augmentation

You can extend existing modules or libraries by declaring additional types and functionality:

Example

// 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);

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
  • Keep merged interfaces focused and cohesive
  • Avoid deep nesting in merged types
  • Use type aliases for simple type combinations instead of merging

Previous

TypeScript Index Signatures

Next

TypeScript Async Programming

This chapter

Overview
40

Lessons

241m

Read time

1. TypeScript Tutorial2. TypeScript Introduction3. TypeScript Getting Started4. TypeScript Simple Types5. TypeScript Explicit Types and Inference6. TypeScript Special Types7. TypeScript Arrays8. TypeScript Tuples9. TypeScript Object Types10. TypeScript Enums11. TypeScript Type Aliases and Interfaces12. TypeScript Union Types13. TypeScript Functions14. TypeScript Casting15. TypeScript Classes16. TypeScript Basic Generics17. TypeScript Utility Types18. TypeScript Keyof19. TypeScript Null & Undefined20. TypeScript Definitely Typed21. TypeScript 5.x Updates22. TypeScript Configuration23. TypeScript with Node.js24. TypeScript with React25. TypeScript Tooling26. TypeScript Advanced Types27. TypeScript Type Guards28. TypeScript Conditional Types29. TypeScript Mapped Types30. TypeScript Type Inference31. TypeScript Literal Types32. TypeScript Namespaces33. TypeScript Index Signatures34. TypeScript Declaration Merging35. TypeScript Async Programming36. TypeScript Decorators37. TypeScript in JavaScript Projects (JSDoc)38. TypeScript Migration Guide39. TypeScript Error Handling40. TypeScript Best Practices

On this page

Understanding Declaration MergingKey BenefitsCommon Use CasesInterface MergingFunction Overloads with MergingNamespace MergingClass and Interface MergingEnum MergingModule AugmentationBest PracticesPerformance Considerations