Module augmentation

Adds members to an existing module's types from your own code, without forking the package's declarations.

Signature
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

ParameterTypeDescription
$"existing"string literalThe 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.
$bodydeclaration blockPatches 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 globaloptionalkeyword blockThe 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.user on Express's Request, and you augment Request so 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

Example · typescript
// 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);
// -> 42

When 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.

Example · typescript
// 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.

Example · typescript
// 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

  • Be the first to comment on this lesson.