Chapter 10.6☕ 14 min read

Declaration Merging

Add properties to existing interfaces, like adding a new room to the Charminar without rebuilding it.

01Adding Rooms to an Existing Building

TypeScript has a unique feature called declaration merging: when you declare the same interface (or namespace, or enum) multiple times, TypeScript automatically combines them into a SINGLE definition. It's like adding a new room to an existing building without having to rebuild the entire structure.

Imagine the Charminar in Hyderabad. It's a historic monument with four grand arches. Now suppose the city wants to add a small museum inside — they don't tear down the Charminar and rebuild it. They ADD to the existing structure while keeping everything that was already there. The Charminar remains the Charminar, just with an additional feature.

Declaration merging works exactly the same way. You have an existing interface — interface User { name: string }. You can't — or don't want to — modify the source file that defines it. Instead, you declare the SAME interface again elsewhere: interface User { age: number }. TypeScript merges them, and now User has both name and age. No modification of the original source, no forking, no editing node_modules. You simply added a room to the existing building.

This is incredibly useful when working with third-party libraries. Express's Request type doesn't have a user property by default, but your auth middleware adds one. Instead of forking Express types, you use declaration merging (module augmentation) to add user?: User to the Request interface. The library types are extended without modifying the library. This is one of TypeScript's most practical and elegant features.

02What is Declaration Merging?

Let's see declaration merging in action with interfaces:

// File: user-base.ts
interface User {
  name: string;
  email: string;
}

// File: user-extended.ts
// SAME interface, same scope
interface User {
  age: number;
  isAdmin: boolean;
}

// File: usage.ts
// User is MERGED automatically:
const user: User = {
  name: "Imran",     // From first declaration
  email: "i@e.com",  // From first declaration
  age: 25,           // From second declaration
  isAdmin: true,     // From second declaration
};

How Merging Works:

  1. Both interface declarations MUST have the same name
  2. Both MUST be in the same scope (same module or global scope)
  3. Properties with the same name MUST have the same type
  4. The merged interface has ALL properties from ALL declarations
  5. Order doesn't matter — all properties are merged

Merging with Namespaces:

// Namespace + Class merge
class Product {}
namespace Product {
  export const create = (
    name: string
  ) => new Product();
}

// Usage:
const p = Product.create("Biryani");

Merging with Enums:

enum Color {
  Red,
  Green,
}

enum Color {
  Blue,   // Merged
  Yellow, // Merged
}

// Color has: Red, Green, Blue, Yellow
03Common Merging Patterns

Let's explore the most practical use cases for declaration merging.

Pattern 1: Module Augmentation (Extending Third-Party Libraries)

// express.d.ts
import { User } from "./types";

declare module "express" {
  interface Request {
    user?: User;
    startTime?: number;
  }
}

// Usage in routes:
app.get("/profile", (req, res) => {
  // req.user is available!
  // req.startTime is available!
  console.log(req.startTime);
});

Pattern 2: Global Augmentation

// global.d.ts
declare global {
  interface Window {
    __APP_VERSION__: string;
    __ENV__: "dev" | "prod";
  }

  namespace NodeJS {
    interface ProcessEnv {
      MY_CUSTOM_VAR: string;
    }
  }
}

// Now you can access:
console.log(window.__APP_VERSION__);
console.log(process.env.MY_CUSTOM_VAR);

Pattern 3: Adding Static Methods to Classes via Namespace

class ApiClient {
  private baseUrl: string;

  constructor(baseUrl: string) {
    this.baseUrl = baseUrl;
  }

  async get(path: string) {
    return fetch(`${this.baseUrl}${path}`);
  }
}

// Add static factory via merging
namespace ApiClient {
  export function create(
    env: "dev" | "prod"
  ): ApiClient {
    const url = env === "dev"
      ? "http://localhost:3000"
      : "https://api.example.com";
    return new ApiClient(url);
  }
}

// Usage:
const client = ApiClient.create("dev");

Pattern 4: Extending Array/Function Types

// Augment Array to add custom method
declare global {
  interface Array<T> {
    first(): T | undefined;
    last(): T | undefined;
  }
}

// Implementation
Array.prototype.first = function<T>() {
  return this[0];
};

Array.prototype.last = function<T>() {
  return this[this.length - 1];
};

// Usage:
const arr = [1, 2, 3];
console.log(arr.first()); // 1
console.log(arr.last());  // 3
04Declaration Merging Traps

Declaration merging is powerful, but it has rules and traps to be aware of.

Trap 1: Type Aliases Don't Merge — Use interfaces instead.

Trap 2: Conflicting Property Types — Same property name across declarations must have matching types.

Trap 3: Module Augmentation Without declare — Must use declare module "name" syntax.

Trap 4: Accidental Global Pollution — Augmenting global types affects all files in the project. Use cautioned.

05Declaration Merging Cheatsheet

What Can Merge:

Interface + Interface = Merged interface
Namespace + Namespace = Merged namespace
Enum + Enum = Merged enum
Class + Namespace = Class with static methods

Module Augmentation Pattern:

declare module "library-name" {
  interface ExistingType {
    newProp: Type;
  }
}

Key Rules:

  • Only interfaces, namespaces, and enums can merge
  • Same property across declarations must have matching types
  • Use declare module "x" to augment third-party libraries
  • Use declare global to augment global types like Window
  • Class + Namespace merging adds static methods to classes

The Golden Rule: "Declaration merging is like adding a room to Charminar — the original structure stays intact, you just add to it. Extend interfaces, augment library types, build on existing foundations without breaking anything, bhai!"

Key Takeaways

  • Declaration merging combines multiple interface declarations into a single type
  • Only interfaces, namespaces, and enums support merging — type aliases do not
  • Use declare module to augment third-party library types without modifying source
  • Use declare global to extend global types like Window, String, Array
  • Same property across merged declarations must have compatible types
  • Class + Namespace merging adds static methods to class definitions
Course Search
Search across all chapters & stages
📖

Search the course

Type any topic — branching, stash, rebase, hooks — and jump straight to that chapter.

merge branchesgit stashundo commitrebase