TypeScript Declaration Merging
Combining multiple declarations into a single definition
🔗 What is Declaration Merging?
Declaration merging allows TypeScript to combine multiple declarations with the same name into one definition. This powerful feature lets you extend interfaces, namespaces, and modules seamlessly throughout your codebase.
// First declaration
interface User {
name: string;
}
// Second declaration - merges with first
interface User {
age: number;
}
// Both properties are now available
const user: User = {
name: "Alice",
age: 25
};
Output:
{ name: "Alice", age: 25 }
Types of Declaration Merging
Interface Merging
Combine multiple interface declarations
interface Box {
height: number;
}
interface Box {
width: number;
}
Namespace Merging
Extend namespaces with new members
namespace Animals {
export class Dog {}
}
namespace Animals {
export class Cat {}
}
Module Augmentation
Add properties to existing modules
declare module 'express' {
interface Request {
user?: User;
}
}
Global Augmentation
Extend global scope types
declare global {
interface Window {
myApp: object;
}
}
🔹 Interface Merging Example
The most common use of declaration merging is with interfaces:
// Define basic user properties
interface User {
id: number;
name: string;
}
// Add authentication properties
interface User {
email: string;
password: string;
}
// Add profile properties
interface User {
avatar?: string;
bio?: string;
}
// All properties are merged
const newUser: User = {
id: 1,
name: "John Doe",
email: "[email protected]",
password: "secret123",
avatar: "profile.jpg"
};
Output:
User has all 6 properties available
🔹 Namespace Merging
Namespaces can be merged to organize related functionality:
// First namespace declaration
namespace MathUtils {
export function add(a: number, b: number): number {
return a + b;
}
}
// Second namespace declaration - merges with first
namespace MathUtils {
export function subtract(a: number, b: number): number {
return a - b;
}
}
// Both functions are available
console.log(MathUtils.add(5, 3)); // 8
console.log(MathUtils.subtract(5, 3)); // 2
Output:
8 2
🔹 Module Augmentation
Extend existing modules with new properties:
// Augment the Array interface
declare global {
interface Array<T> {
first(): T | undefined;
last(): T | undefined;
}
}
// Implement the methods
Array.prototype.first = function() {
return this[0];
};
Array.prototype.last = function() {
return this[this.length - 1];
};
// Use the new methods
const numbers = [1, 2, 3, 4, 5];
console.log(numbers.first()); // 1
console.log(numbers.last()); // 5
Output:
1 5
🔹 Merging Rules
Important rules to remember when merging declarations:
✅ What Can Be Merged:
- Interfaces: Always merge together
- Namespaces: Merge with other namespaces
- Namespaces with Classes: Extend classes
- Namespaces with Functions: Add properties to functions
❌ What Cannot Be Merged:
- Classes: Cannot merge with other classes
- Type Aliases: Cannot be merged
- Conflicting Properties: Same property with different types
// ❌ This will cause an error
interface Product {
price: number;
}
interface Product {
price: string; // Error: Duplicate property with different type
}
// ✅ This is correct
interface Product {
price: number;
}
interface Product {
name: string; // OK: Different property
}
🔹 Practical Use Case
Extending third-party library types:
// Extend Express Request type
import { Request } from 'express';
declare module 'express-serve-static-core' {
interface Request {
user?: {
id: number;
username: string;
};
}
}
// Now you can use the custom property
function authMiddleware(req: Request) {
if (req.user) {
console.log(`User ${req.user.username} is authenticated`);
}
}
Benefit:
Type-safe access to custom properties on Request