TypeScript Declaration Merging

Declaration merging in TypeScript is a powerful feature that allows multiple declarations of the same name to be merged into a single declaration. This capability is particularly useful when working with complex types, extending existing libraries, and creating flexible APIs. This chapter will provide a comprehensive understanding of declaration merging in TypeScript, starting from basic concepts to advanced use cases, with detailed explanations, examples, and code outputs.

Basics of Declaration Merging

What is Declaration Merging?

Declaration merging occurs when the TypeScript compiler merges multiple declarations of the same name into a single entity. This can happen with interfaces, functions, classes, enums, and namespaces. The merged declaration combines the properties and behaviors of all individual declarations.

Simple Example of Declaration Merging

Let’s start with a basic example involving interfaces.

				
					// First declaration
interface User {
    name: string;
}

// Second declaration
interface User {
    age: number;
}

// Merged interface
const user: User = {
    name: 'Alice',
    age: 30
};

console.log(user);
				
			

Explanation:

  • We have two separate declarations of the User interface.
  • TypeScript merges these declarations into a single interface with both name and age properties.
  • We create an object user that conforms to the merged User interface.

Output:

				
					{ name: 'Alice', age: 30 }
				
			

Declaration Merging with Interfaces

Merging Properties

When interfaces with the same name are declared, their properties are merged.

Example

				
					// First declaration
interface Point {
    x: number;
}

// Second declaration
interface Point {
    y: number;
}

// Merged interface
const point: Point = {
    x: 10,
    y: 20
};

console.log(point);
				
			

Explanation:

  • Two Point interfaces are declared with different properties.
  • TypeScript merges them into a single interface with both x and y properties.
  • We create an object point that has both properties.

Output:

				
					{ x: 10, y: 20 }
				
			

Extending Interfaces

Interfaces can extend other interfaces, and merged declarations can also include extensions.

Example

				
					// Base interface
interface Shape {
    color: string;
}

// First declaration
interface Circle extends Shape {
    radius: number;
}

// Second declaration
interface Circle {
    circumference: number;
}

// Merged interface
const circle: Circle = {
    color: 'red',
    radius: 10,
    circumference: 62.8
};

console.log(circle);
				
			

Explanation:

  • Circle interface extends Shape and is declared twice with additional properties.
  • TypeScript merges the Circle declarations and includes the properties from Shape.
  • We create an object circle that has color, radius, and circumference properties.

Output:

				
					{ color: 'red', radius: 10, circumference: 62.8 }
				
			

Declaration Merging with Functions

Functions can also be merged. When two functions with the same name are declared, their overloads are combined.

Function Overloads

Example

				
					// First declaration
function add(a: number, b: number): number;

// Second declaration
function add(a: string, b: string): string;

// Implementation
function add(a: any, b: any): any {
    return a + b;
}

console.log(add(1, 2));       // Output: 3
console.log(add('Hello, ', 'World!')); // Output: Hello, World!
				
			

Explanation:

  • Two add function declarations with different parameter types.
  • TypeScript merges these declarations, allowing for function overloading.
  • The implementation handles both number and string additions.

Output:

				
					3
Hello, World!
				
			

Declaration Merging with Namespaces

Namespaces allow grouping of related code. Declaration merging with namespaces combines their contents.

Merging Namespaces

Example

				
					// First declaration
namespace Animals {
    export class Cat {
        meow() {
            console.log('Meow');
        }
    }
}

// Second declaration
namespace Animals {
    export class Dog {
        bark() {
            console.log('Woof');
        }
    }
}

// Using merged namespace
const cat = new Animals.Cat();
const dog = new Animals.Dog();

cat.meow(); // Output: Meow
dog.bark(); // Output: Woof
				
			

Explanation:

  • Two Animals namespaces are declared with different classes.
  • TypeScript merges them into a single Animals namespace containing both Cat and Dog classes.
  • We create instances of Cat and Dog and call their methods.

Output:

				
					Meow
Woof
				
			

Advanced Declaration Merging

Merging Classes

Classes can also participate in declaration merging. However, this is less common and more complex.

Example

				
					// First declaration
class Vehicle {
    wheels: number = 4;
}

// Second declaration
interface Vehicle {
    engine: string;
}

// Merged class and interface
const car: Vehicle = {
    wheels: 4,
    engine: 'V8'
};

console.log(car);
				
			

Explanation:

  • Vehicle class is declared with a wheels property.
  • An interface Vehicle is declared with an engine property.
  • TypeScript merges them into a single type containing both properties.

Output:

				
					{ wheels: 4, engine: 'V8' }
				
			

Merging Enums

Enums can be merged to combine their members.

Example

				
					// First declaration
enum Status {
    Active,
    Inactive
}

// Second declaration
enum Status {
    Pending,
    Completed
}

// Using merged enum
console.log(Status.Active);     // Output: 0
console.log(Status.Pending);    // Output: 2
console.log(Status.Completed);  // Output: 3
				
			

Explanation:

  • Status enum is declared twice with different members.
  • TypeScript merges them into a single enum with all members.
  • We access the merged enum members.

Output:

				
					0
2
3
				
			

Practical Applications of Declaration Merging

Extending Existing Libraries

Declaration merging is useful for extending existing libraries with additional functionality.

Example

Consider an existing library library with a namespace Library.

				
					// library.d.ts
declare namespace Library {
    function existingFunction(): void;
}

// Extending the library
declare namespace Library {
    function newFunction(): void;
}

// Using extended library
Library.existingFunction();
Library.newFunction();
				
			

Explanation:

  • library.d.ts provides the original declaration.
  • We extend the Library namespace with a new function.
  • Both functions are available for use.

Augmenting Global Objects

Declaration merging allows adding properties to global objects.

Example

				
					// Existing global object
declare global {
    interface Window {
        customProperty: string;
    }
}

// Adding a property
window.customProperty = 'Hello, World!';

console.log(window.customProperty);
				
			

Explanation:

  • We declare an augmentation of the global Window interface.
  • A custom property customProperty is added to window.
  • We set and access the new property.

Output:

				
					Hello, World!
				
			

TypeScript's declaration merging is a versatile feature that allows combining multiple declarations of the same name into a single entity. This chapter covered the basics of declaration merging with interfaces, functions, namespaces, classes, and enums, along with advanced use cases and practical applications. Happy coding! ❤️

Table of Contents