Introduction
Sometimes you need to add types to existing modules or the global scope — extending Express's Request interface, adding properties to Window, or augmenting a third-party library's types. TypeScript's declaration merging and module augmentation let you do this safely.
Key Concepts
- Module Augmentation: Adding new exports or extending existing types of an external module using
declare module "...". - Global Augmentation: Adding types to the global scope using
declare global { ... }inside a module file. - Declaration Merging: TypeScript's ability to combine multiple declarations of the same name into a single definition.
Real World Context
Express middleware like passport adds a user property to req. Without augmentation, TypeScript does not know req.user exists. Module augmentation lets you declare req.user: User so that all your route handlers get type-safe access.
Deep Dive
Module Augmentation
Extend a third-party module's types:
typescript// types/express.d.ts import { User } from "../models/user"; declare module "express-serve-static-core" { interface Request { user?: User; sessionId: string; } }
Now req.user and req.sessionId are available in all Express route handlers with full type information.
Global Augmentation
Add to the global scope from within a module file:
typescript// types/global.d.ts export {}; // Makes this a module (required for declare global) declare global { interface Window { analytics: { track(event: string, data?: Record<string, unknown>): void; }; } // Add a global utility type type Nullable<T> = T | null; }
The export {} is critical — without it, the file is a script (not a module), and declare global is not needed.
Interface Merging
TypeScript merges multiple interface declarations with the same name:
typescriptinterface Config { apiUrl: string; } interface Config { debug: boolean; } // Merged result: // interface Config { apiUrl: string; debug: boolean; }
This works because interfaces are open-ended in TypeScript.
Common Pitfalls
- Missing
export {}in global augmentation files — Without it, the file is treated as a script anddeclare globalis unnecessary (but confusing). Always includeexport {}. - Augmenting the wrong module name — Express types live in
express-serve-static-core, notexpress. Check the source.d.tsfiles to find the correct module name.
Best Practices
- Keep augmentations in a
types/directory — Centralize all augmentation files for discoverability. - Reference augmentation files in tsconfig — Ensure your
includepattern covers thetypes/directory.
Summary
declare module "..."extends third-party module types.declare globaladds to the global scope from within a module.- Always include
export {}in global augmentation files. - Interface merging lets you extend interfaces across multiple declarations.
Code Examples
// types/express.d.ts — augment Express Request
import { User } from "../models/user";
declare module "express-serve-static-core" {
interface Request {
user?: User;
}
}
// Now in any route handler:
import { Request, Response } from "express";
app.get("/profile", (req: Request, res: Response) => {
if (req.user) {
res.json({ name: req.user.name }); // Fully typed!
}
});