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.
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:
// 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.
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.
// 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.
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:
// main.ts
import "math-lib"; // the actual library
import "./math-lib.augment"; // trigger the augmentationIf 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:
// 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:
- No new top-level declarations. You can only patch existing exports. You cannot add a brand-new export that did not exist before.
declare module "math-lib" {
export function factorial(n: number): number;
// ^ This is NOT allowed if factorial was not already exported.
}- 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.
// 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:
| Feature | Module Augmentation | Global Augmentation |
|---|---|---|
| Syntax | declare module with a package name | declare global block |
| Scope | One package only | Entire global scope |
| Use case | Extending a library's types | Adding window or built-in globals |
| Example | Add method to a class from lodash | Add 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.
// 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.
// 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.
// 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.
// 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
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.
Frequently Asked Questions
Can I add new exports to a module through augmentation?
Does module augmentation work with default exports?
Do I need to import the module to augment it?
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.
More in this topic
TypeScript Compiler Explained
The TypeScript compiler (tsc) turns .ts files into .js files while checking types. Learn what it does, how to use it, and how it fits your workflow.
Set Up ESLint for TypeScript
Install and configure ESLint with typescript-eslint to catch bugs and enforce consistent code style in your TypeScript project.
Pick Utility Type in TypeScript
Pick creates a new type by selecting only the properties you name from an existing type. Use it to build focused types for specific views, API responses, or function parameters.