Declaration Merging & Module Augmentation

How TypeScript merges interfaces, augments modules (like Express/NextAuth), and why this matters in real codebases.

F

Frontend Interview Team

February 08, 2026

2 min read
Declaration Merging & Module Augmentation

What you’ll learn

  • What declaration merging is (and what it isn’t)
  • How module augmentation works in real projects
  • When to use it (and when to avoid it)
  • Interview-ready examples + pitfalls

30‑second interview answer

Declaration merging is TypeScript’s feature where multiple declarations with the same name (like interfaces) are merged into a single type. Module augmentation is a controlled way to add types to an existing module’s declarations (e.g., adding fields to Express.Request or extending next-auth session types). It’s powerful for library integration, but should be used carefully to avoid global type pollution.


Mental model

TypeScript has two worlds:

  • Value space (runtime): variables, functions, classes
  • Type space (compile-time): types, interfaces, type declarations

Merging happens in the type space — it does not change runtime behavior.


1) Declaration merging (interfaces)

interface User {
  id: string;
}
 
interface User {
  name: string;
}
 
// Merged:
// interface User { id: string; name: string }

Interview points:

  • Interfaces merge.
  • type aliases do not merge.

2) Function overload merging

function parse(input: string): string;
function parse(input: number): number;
function parse(input: string | number) {
  return input;
}

Overloads are a form of “multiple declarations” that TypeScript uses to build the final call signatures.


3) Namespace merging (common in older libs)

function greet(name: string) {
  return `Hi ${name}`;
}
 
namespace greet {
  export const version = '1.0.0';
}
 
greet.version; // ok

This is more legacy, but still appears.


4) Module augmentation (the real-world use case)

Example: Express Request

// types/express.d.ts
import 'express-serve-static-core';
 
declare module 'express-serve-static-core' {
  interface Request {
    userId?: string;
  }
}

Now everywhere in your app:

app.get('/me', (req, res) => {
  req.userId; // string | undefined
});

Example: Adding types to a library’s config

You might extend a library’s types so your app-specific fields are strongly typed.


Where to put augmentation files

Best practice:

  • Put *.d.ts in a dedicated folder like types/.
  • Ensure it’s included by TS (via tsconfig.json include or typeRoots).

Example tsconfig.json:

{
  "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", "types/**/*.d.ts"]
}

Common pitfalls

  • Augmenting the wrong module name (augmentation silently doesn’t apply)
  • Forgetting to include *.d.ts in tsconfig
  • Creating global conflicts by augmenting too broadly
  • Using augmentation to “paper over” bad types instead of fixing upstream

Rule of thumb:

  • Use augmentation when you integrate with a library and need to reflect app-specific fields.
  • Avoid using it as a shortcut for local types.

Interview questions to practice

  1. Why do interfaces merge but type aliases don’t?
  2. What’s module augmentation used for?
  3. How do you ensure your *.d.ts file is picked up by TS?
  4. What are the risks of global augmentation?

Quick recap

  • Declaration merging merges compatible declarations (mostly interfaces).
  • Module augmentation extends an existing module’s types.
  • Powerful for integrations, risky if overused.