Module Augmentation & Declaration Merging
debt(d7/e3/b3/t7)
Closest to 'only careful code review or runtime testing' (d7). The detection_hints indicate automated detection is 'no' and the code_pattern targets symptom workarounds like `req as any` rather than the misconfigured augmentation itself. TypeScript won't error loudly when augmentation is set up incorrectly in a script file — it simply silently ignores the declarations, meaning the missing types only surface when developers notice `any` spreading or missing properties during code review or testing, not at compile time with a clear error.
Closest to 'simple parameterised fix' (e3). The quick_fix describes creating a .d.ts file with a specific declare module pattern and ensuring export {} is present, plus including it in tsconfig. This is slightly more than a one-line patch (it may touch a new file and tsconfig), but it's contained within one component/configuration area without cross-cutting changes.
Closest to 'localised tax' (b3). Once the augmentation .d.ts file is correctly set up, it stays localized to type infrastructure. It applies to web and cli contexts but the structural weight is limited — maintainers only need to be aware of where augmentation files live. It's not a strong architectural gravitational pull, just a small ongoing tax if the augmented interfaces evolve.
Closest to 'serious trap' (t7). The misconception field shows developers incorrectly believe they must fork library types, and the common_mistakes reveal multiple non-obvious pitfalls: augmentation silently does nothing in script files (no import/export), the module path string must exactly match, and augmentation cannot remove or override existing declarations. These behaviors contradict intuitive expectations — a developer who writes `declare module` in a plain .ts file will see no error but get no augmentation, which contradicts how most TypeScript constructs behave.
Also Known As
TL;DR
Explanation
Declaration merging: two interface declarations with the same name are merged into one. Module augmentation: declare module 'express' { interface Request { user?: User } } adds user to Express's Request without forking. Global augmentation: declare global { interface Window { analytics: Analytics } } extends the DOM's Window. Must be in a module file (has import/export) — use export {} to make a plain .ts file a module. Use cases: Express/Fastify request decorators, Vue/Nuxt global properties ($store, $router), env variables on process.env, custom jest matchers. Ambient declarations (declare module '*.svg') type non-JS imports.
Diagram
flowchart TD
LIB[express/index.d.ts<br/>interface Request - base]
AUG[types/express.d.ts<br/>declare module express<br/>interface Request - user User]
MERGED[Merged Request<br/>base fields + user: User]
LIB --> MERGED
AUG --> MERGED
MERGED --> APP[All req objects fully typed]
Watch Out
Common Misconception
Why It Matters
Common Mistakes
- Writing augmentation in a script file (no import/export) instead of a module file — declare module has no effect in script files.
- Augmenting the wrong module path — must exactly match the string used in import.
- Trying to remove or override existing declarations — augmentation only adds, never replaces.
- Forgetting to include the augmentation .d.ts in tsconfig include — the file must be picked up by the compiler.
Avoid When
- When augmenting would hide a genuine type incompatibility — don't use augmentation to silence errors.
- For types you own — just edit the source directly.
When To Use
- Adding custom properties to Express/Fastify Request or Response in middleware.
- Typing global variables injected by build tools (import.meta.env, __webpack_public_path__).
- Extending third-party interfaces (Vue component options, Jest matchers) without forking.
Code Examples
// Unsafe — req.user typed as any everywhere
app.use((req: any, res, next) => {
req.user = getUserFromToken(req.headers.authorization);
next();
});
// Later:
const user = req.user; // any — no type safety
// types/express.d.ts — module augmentation
import { User } from './models';
declare module 'express-serve-static-core' {
interface Request {
user?: User;
}
}
export {}; // makes this a module file
// Now everywhere in the app:
app.use((req, res, next) => {
req.user = getUserFromToken(req.headers.authorization); // typed
next();
});
router.get('/profile', (req, res) => {
const user = req.user; // User | undefined — fully typed
});