TypeScript

Module augmentation and declaration merging

TypeScript lets multiple separate declarations of the same interface (even across different files) combine into one — usually invisible and automatic, but it's exactly the real, sanctioned mechanism for adding your own properties to a type a LIBRARY defined, without ever touching that library's own source.

Advanced

3 min read

Declaration merging: two interface declarations with the same name combine

interface User {
  name: string;
}
 
interface User {
  age: number;
}
 
const u: User = { name: "Ada", age: 36 }; // BOTH properties required — the two declarations MERGED

This isn't an error, and it isn't the second interface overriding the first — TypeScript specifically merges multiple interface declarations sharing the same name into one combined shape, requiring every property from every declaration. This is genuinely different from type aliases, which cannot be declared twice with the same name at all — interface's mergeability is a deliberate, real design choice, not an accidental quirk.

Module augmentation: using merging to add properties to a TYPE FROM A LIBRARY

// your-project/express.d.ts
import "express";
 
declare module "express" {
  interface Request {
    user?: { id: string; role: string };  // adding a property Express's OWN types don't define
  }
}
 
// anywhere else in your project:
app.get("/profile", (req, res) => {
  console.log(req.user?.id); // TypeScript now recognizes req.user — WITHOUT editing express's own source
});

Module augmentation uses declare module to reopen a module's own type declarations from outside that module's source code — commonly used to add a property an authentication middleware attaches to Express's Request object at runtime, which Express's own type definitions naturally have no way to know about. This is the real, sanctioned way to extend a third-party library's types: TypeScript merges your declare module block's interface Request with Express's own interface Request declaration, exactly the same merging mechanism as the plain example above, just reaching across a module boundary.

Why this is different from — and safer than — just casting

// The tempting but genuinely worse alternative — casting away type safety at EVERY use site
app.get("/profile", (req: any, res) => {   // `any` — loses ALL type checking on req, everywhere
  console.log(req.usr?.id); // TYPO — "usr" instead of "user" — silently compiles, `any` catches nothing
});

Casting req to any (or a custom type) at every single call site where the extra property is needed loses type-checking entirely for that request object — a typo like req.usr (instead of req.user) compiles without complaint, since any disables checking completely. Module augmentation fixes the actual type definition once, in one file, and every use of Request throughout the entire codebase gets the real, checked user property — including autocomplete and typo detection — without any local casting or any needed anywhere.

The real, common use case: augmenting a global object or a library's config type

// Extending the global Window object with a property a third-party script attaches
declare global {
  interface Window {
    analyticsId?: string;
  }
}
 
window.analyticsId = "UA-12345"; // now type-checked, instead of needing (window as any).analyticsId

The exact same merging mechanism extends genuinely global declarations too — declare global reopens the ambient Window interface, letting window.analyticsId (a property some third-party script attaches at runtime, invisible to TypeScript's own built-in lib.dom.d.ts definitions) be used with full type-checking, instead of requiring (window as any).analyticsId scattered everywhere it's actually used.

Further reading

Check your understanding

A quick comprehension check — not tracked, not graded, just for you.

1. What happens when two `interface` declarations share the same name in TypeScript?

2. What does module augmentation with `declare module` actually let you do?

3. Why is module augmentation safer than casting to `any` at every use site needing an extra property?