Module augmentation
Adds members to an existing module's types from your own code, without forking the package's declarations.
import { Thing } from "./thing.js";
declare module "./thing.js" {
interface Thing { extra(): void }
}
declare global {
interface Array<T> { last(): T | undefined }
}- Added in TypeScript 1.8
Parameters
| Parameter | Type | Description |
|---|---|---|
$"existing" | string literal | The specifier of a module that already exists. It is resolved exactly like an import specifier, so a relative path is resolved against the augmenting file. |
$body | declaration block | Patches to declarations the target module already exports — typically `interface` or `namespace` members that merge with the originals. New top-level declarations are not allowed. |
$declare globaloptional | keyword block | The global variant: adds members to global interfaces such as `Window`, `Array` or `globalThis` from inside a module. |
Errors
Raises "Cannot augment module 'x' because it resolves to a non-module entity" or "Cannot augment module 'x' with value exports because it resolves to a non-module entity" when the target is not a module, and "Augmentations for the global scope can only be directly nested in external modules or ambient module declarations" when `declare global` is used in a script file.
Changelog
- PHP 1.8Module augmentation and `declare global` introduced.
- PHP 4.7`--moduleDetection` added; `force` makes every file a module, so `declare module "x"` in a previously-script file becomes an augmentation and now requires `x` to already exist.
Module augmentation lets you add to a module's type declarations from outside it. The file must itself be a module — it needs a top-level import or export — and the target module must already exist. That is the whole difference from an ambient declaration: an ambient module invents a module; an augmentation patches one.
When to reach for it
- A plugin adds members at runtime. The classic case: middleware hangs
req.useron Express'sRequest, and you augmentRequestso the property type-checks everywhere. - You are monkey-patching a prototype and want the type to follow the patch.
- A library's types are missing a member you know exists, and forking the package is not worth it.
What trips people up
Three rules bite repeatedly. First, you can only patch, not add: a brand-new top-level declaration inside the block is an error. Second, you cannot augment a default export, since augmentation works by exported name and default is reserved — augment a named export instead. Third, the augmenting file must be loaded. If nothing imports it, TypeScript never sees the augmentation and your property vanishes; the fix is a side-effect import (import "./augmentations.js") or listing the file in tsconfig's include.
The specifier must also match how the module resolves — augmenting "express" and "express/index.js" are not the same target.
Example
// observable.ts
export class Observable<T> { constructor(public value: T) {} }
// map.ts — augments the class's instance type
import { Observable } from "./observable.js";
declare module "./observable.js" {
interface Observable<T> { map<U>(f: (x: T) => U): Observable<U> }
}
Observable.prototype.map = function (f) { return new Observable(f(this.value)); };
// consumer.ts
import { Observable } from "./observable.js";
import "./map.js"; // side-effect import loads the augmentation
console.log(new Observable(21).map((x) => x * 2).value);
// -> 42When to use it
- Add req.user to Express's Request after an authentication middleware sets it
- Type a prototype method you added to Array or String via declare global
- Declare extra fields on a framework's config or context interface that a plugin injects
- Add missing members to a dependency's types without forking or patching node_modules
Frequently asked questions
Why is my module augmentation being ignored?
Almost always because the file containing it is not in the program. Add a side-effect import (`import "./augmentations.js"`) somewhere in your source, or make sure tsconfig's `include` or `files` covers the declaration file. A `.d.ts` sitting in the repo that nothing references is invisible to the compiler.
How do I add a property to a third-party library's interface?
Create a file that is a module (it has a top-level import or export), then write `declare module "the-package" { interface TheInterface { yourProp: T } }`. The interface merges with the original, so every use site in your project sees the new member. You cannot augment a default export or add new top-level declarations this way.
What is the difference between declare module and module augmentation?
They are the same syntax with different meaning depending on the file. In a script file — no top-level import or export — `declare module "x"` declares a brand-new ambient module. In a module file it augments an existing module `x`, and errors if `x` cannot be resolved.
More examples
Adding req.user to Express
The augmentation targets `express-serve-static-core`, not `express` — that is where `@types/express` actually declares `Request`, and augmenting the wrong specifier compiles but changes nothing. Declaring `user` as optional keeps the type honest: the property only exists after the auth middleware has run, and `req.user?.id` forces callers to handle the unauthenticated path.
// types/express.d.ts
import "express"; // makes this file a module and loads the target
declare module "express-serve-static-core" {
interface Request {
user?: { id: string; roles: string[] };
}
}
// auth.ts
import type { Request, Response, NextFunction } from "express";
export function requireAdmin(req: Request, res: Response, next: NextFunction) {
// req.user now type-checks and is correctly optional
if (!req.user?.roles.includes("admin")) {
console.log("denied", req.user?.id ?? "anonymous");
return res.status(403).end();
}
console.log("allowed", req.user.id);
next();
}declare global, and the two things that break it
The `export {}` at the top is load-bearing: without it the file is a script and `declare global` is an error. The commented `const` shows the second limit — a global augmentation may only contain ambient declarations, not new runtime values. And the side-effect import in app.ts is what actually pulls the augmentation into the program; drop it and `.last()` becomes a compile error again even though the prototype patch would still run.
// array-extras.ts
export {}; // required: declare global only works inside a module
declare global {
interface Array<T> {
last(): T | undefined;
}
// tsc: Augmentations for the global scope can only contain ambient declarations
// const NOT_ALLOWED = 1;
}
Array.prototype.last = function () {
return this.length ? this[this.length - 1] : undefined;
};
// app.ts
import "./array-extras.js"; // without this import, .last() does not exist
console.log([1, 2, 3].last());
// -> 3
console.log([].last());
// -> undefined
Discussion