Declaration Merging & Module Augmentation
How TypeScript merges interfaces, augments modules (like Express/NextAuth), and why this matters in real codebases.
Frontend Interview Team
February 08, 2026
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.
typealiases 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; // okThis 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.tsin a dedicated folder liketypes/. - Ensure it’s included by TS (via
tsconfig.jsonincludeortypeRoots).
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.tsintsconfig - 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
- Why do interfaces merge but type aliases don’t?
- What’s module augmentation used for?
- How do you ensure your
*.d.tsfile is picked up by TS? - 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.