Module Augmentation in TypeScript

Module augmentation lets you add new types to existing modules without modifying their source code. Learn how to extend third-party packages and your own modules with declare module.

7 min read

Module augmentation is a TypeScript feature that lets you add new types, properties, or overloads to an existing module without editing its source code. The magic is declare module, which opens a module's type space and lets you contribute to it from any file in your project.

When you write declare module with a package name, TypeScript merges your additions with the module's original types as if they were declared in the same file.

The original package stays untouched. Your types layer on top.

When Module Augmentation Is Useful

You need module augmentation in these situations:

  • A library's official types are missing a method you added at runtime by patching a prototype.
  • A framework like Express or Vue lets plugins extend core interfaces, and you want type safety for those extensions.
  • You want to add new properties to a class exported by a package you do not control.
  • You are writing a plugin and want consumers to get autocomplete for the properties your plugin adds.

The common thread: the runtime behavior is already there (or you are adding it), but the TypeScript types do not reflect it.

Basic Module Augmentation

Imagine you use a library called math-lib that exports a Calculator class. The official types include add and subtract, but you have patched a multiply method onto the prototype at runtime:

typescripttypescript
// math-lib's official types (you cannot edit this):
// export class Calculator {
//   add(a: number, b: number): number;
//   subtract(a: number, b: number): number;
// }

Without augmentation, TypeScript only knows about the two methods in the official types, so it rejects any call to the method you patched on at runtime.

typescripttypescript
import { Calculator } from "math-lib";
 
const calc = new Calculator();
calc.multiply(3, 4);
//   ^ Property 'multiply' does not exist on type 'Calculator'.

Add an augmentation file to teach TypeScript about the new method. It targets the same module name and interface, so TypeScript merges the extra method signature into the existing type.

typescripttypescript
// math-lib.augment.d.ts
declare module "math-lib" {
  interface Calculator {
    multiply(a: number, b: number): number;
  }
}

Now that the augmentation is in place, TypeScript accepts calls to both the original methods and the patched one, all with full type checking.

typescripttypescript
import { Calculator } from "math-lib";
 
const calc = new Calculator();
calc.add(1, 2);       // number (from original types)
calc.multiply(3, 4);  // number (from augmentation)

No changes to node_modules. The original library is untouched. The augmentation file lives in your project and only affects type checking.

How the Augmentation Is Consumed

The augmentation file must be included in your TypeScript compilation. The simplest way is to make sure it is covered by your tsconfig.json include pattern. You do not need to import the augmentation file explicitly, TypeScript picks it up automatically if the file is in scope.

If the augmentation file is a module (has imports or exports), you need to import it somewhere so the augmentation takes effect:

typescripttypescript
// main.ts
import "math-lib";             // the actual library
import "./math-lib.augment";   // trigger the augmentation

If the augmentation file is a script (no imports/exports), it applies globally without being imported.

Augmenting with Function Overloads

You can add new overload signatures to an existing function:

typescripttypescript
// logger.augment.d.ts
declare module "logger-lib" {
  function log(level: "info", message: string): void;
  function log(level: "error", message: string, error: Error): void;
}

These overloads merge with any existing overloads from the original types. TypeScript treats the combined list as the full set of valid call signatures.

The Two Limits of Module Augmentation

Module augmentation has two important restrictions:

  1. No new top-level declarations. You can only patch existing exports. You cannot add a brand-new export that did not exist before.
typescripttypescript
declare module "math-lib" {
  export function factorial(n: number): number;
  // ^ This is NOT allowed if factorial was not already exported.
}
  1. Default exports cannot be augmented. The export name for a default is the reserved word default, and you cannot reference it in a declare module block.

These limits exist because augmentation merges with existing declarations. If there is nothing to merge with, the augmentation has no anchor.

Real-World Pattern: Express Request Augmentation

Many Express middleware libraries add properties to the Request object. TypeScript needs to know about these properties for them to be available in route handlers.

typescripttypescript
// express-session adds req.session at runtime, but the types need help.
declare module "express-serve-static-core" {
  interface Request {
    session: {
      userId: string;
      destroy(): void;
    };
  }
}

After this augmentation, every Express route handler sees req.session with full type information. The actual property is set at runtime by the express-session middleware. The augmentation simply tells TypeScript it will be there.

For more on real-world patterns like this, see declaration files in TypeScript.

Module Augmentation vs Global Augmentation

Module augmentation targets a specific package. Global augmentation targets the global scope. Here is how they differ:

FeatureModule AugmentationGlobal Augmentation
Syntaxdeclare module with a package namedeclare global block
ScopeOne package onlyEntire global scope
Use caseExtending a library's typesAdding window or built-in globals
ExampleAdd method to a class from lodashAdd a window.analytics property

For adding types to the global scope, see global declarations in TypeScript.

Augmenting Your Own Modules

Module augmentation is not limited to third-party packages. You can augment your own modules, which is useful in monorepos or when different parts of a large codebase need to contribute to a shared interface. The core package below defines an empty registry that plugins can add themselves to.

typescripttypescript
// In the core package:
// core/types.ts
export interface PluginRegistry {
  [pluginName: string]: unknown;
}

Each plugin package then augments that same interface with its own key, without ever editing the core package's source. TypeScript merges every plugin's contribution into one combined registry type at compile time.

typescripttypescript
// In a plugin package:
// plugin-a/augment.d.ts
declare module "@my-org/core/types" {
  interface PluginRegistry {
    pluginA: {
      run(): void;
      version: string;
    };
  }
}

The registry accumulates types from every plugin. Consumers see the merged interface with contributions from all active augmentations. This is the same pattern Vue and Express use internally for their plugin systems.

Common Mistake: Augmenting the Wrong Module

Module names in declare module must match the package name or import path exactly. If the library's types are in a sub-module, like express-serve-static-core for Express, you must augment the sub-module, not the main package. Augmenting the public-facing package name compiles without error but has no effect, since TypeScript never merges it with the actual Request type.

typescripttypescript
// Wrong: augmenting the public-facing package
declare module "express" {
  interface Request {
    userId: string;
  }
}

The fix is to target the internal package that Express actually uses to declare Request, which is express-serve-static-core, not express itself.

typescripttypescript
// Correct: augmenting the internal interface package
declare module "express-serve-static-core" {
  interface Request {
    userId: string;
  }
}

Check the library's type definitions to find the right module to augment. Augmenting the wrong module silently does nothing, with no compiler warning to catch the mistake.

Rune AI

Rune AI

Key Insights

  • declare module "pkg" { ... } lets you add types to an existing module.
  • You can only patch existing declarations, not add new top-level exports.
  • Default exports cannot be augmented.
  • The augmented module is consumed by importing both the original module and the augmentation file.
  • Module augmentation is the standard pattern for adding types to Express Request, Vue components, and other plugin-heavy libraries.
RunePowered by Rune AI

Frequently Asked Questions

Can I add new exports to a module through augmentation?

No. Module augmentation only lets you patch existing declarations -- adding new properties to interfaces, new overloads to functions, or new members to namespaces. You cannot declare brand-new top-level exports.

Does module augmentation work with default exports?

No. Default exports cannot be augmented because the export name is the reserved word default. Augmentation works on named exports where you can reference the export by its declared name.

Do I need to import the module to augment it?

No. The declare module statement references the module by its package name or path as a string literal. You do not need an actual import of the module in the augmentation file, though you usually need an import in the file that applies the augmentation at runtime.

Conclusion

Module augmentation lets you extend the types of existing modules without touching their source code. It is the right tool when a library's types are incomplete, when you need to patch a method onto a prototype, or when you want to add types for a plugin system without forking the original package.