Module Augmentation
Extend third-party and global type declarations without modifying source using module augmentation.
You are an expert in Module Augmentation for writing type-safe TypeScript.
## Key Points
- Place augmentation files in a dedicated `types/` directory and include it in `tsconfig.json`'s `include` or `typeRoots`.
- Always include `export {}` in global augmentation files to ensure they are treated as modules.
- Keep augmentations minimal — only add what your code actually uses, to avoid masking upstream type changes.
- Forgetting `export {}` in a file that uses `declare global`, causing it to be treated as a script and silently failing to augment anything.
- Using `declare module` with a relative path that does not exactly match the module's resolution path — the augmentation will create a new module instead of merging with the existing one.
## Quick Example
```typescript
// types/legacy-lib.d.ts
declare module "legacy-lib" {
export function init(config: { apiKey: string }): void;
export function send(payload: unknown): Promise<{ status: number }>;
}
```skilldb get typescript-patterns-skills/Module AugmentationFull skill: 149 linesModule Augmentation — TypeScript Patterns
You are an expert in Module Augmentation for writing type-safe TypeScript.
Overview
Module augmentation lets you add new declarations to existing modules — both third-party packages and your own — without modifying their source files. It uses declare module "..." blocks to merge new interfaces, types, and function signatures into an existing module's type namespace. Use it to extend library types with custom properties, patch missing declarations, add global types, and integrate plugins.
Core Philosophy
Module augmentation is TypeScript's escape hatch for the real world of third-party code. Libraries ship with type declarations that cover their public API, but your application often needs to extend those types — adding middleware properties to Express requests, registering plugins in a type-safe registry, or patching missing declarations for untyped packages. Module augmentation lets you do this without forking the library or losing type safety.
The mechanism relies on TypeScript's declaration merging: when two declarations of the same interface exist in the same module scope, the compiler merges them into a single interface with all members from both declarations. Module augmentation simply provides a way to add your declarations into another module's scope from the outside. The result is seamless — consumers of the augmented type see a unified interface with no indication that parts came from different files.
Discipline is essential because augmentations are global in effect. Once you augment Express's Request interface, every file in the project sees the new properties. This power should be used sparingly: augment only what your application genuinely needs, keep augmentation files organized in a dedicated directory, and document why each augmentation exists. Overuse can make it difficult to tell which properties come from the library and which from your own augmentations.
Core Concepts
Augmenting a third-party module:
// express-augment.d.ts
import "express";
declare module "express" {
interface Request {
userId?: string;
correlationId?: string;
}
}
After this declaration, every Request object in your codebase includes userId and correlationId.
Augmenting the global scope:
// global.d.ts
export {};
declare global {
interface Window {
analytics: {
track(event: string, data?: Record<string, unknown>): void;
};
}
}
The export {} ensures the file is treated as a module, which is required for declare global to work.
Augmenting your own modules:
// types/base.ts
export interface AppConfig {
port: number;
host: string;
}
// types/database-plugin.ts
import "./base";
declare module "./base" {
interface AppConfig {
dbConnectionString: string;
dbPoolSize: number;
}
}
TypeScript merges the two AppConfig declarations automatically via declaration merging.
Implementation Patterns
Plugin system with augmented registry:
// plugin-registry.ts
export interface PluginRegistry {}
export function getPlugin<K extends keyof PluginRegistry>(name: K): PluginRegistry[K] {
// runtime lookup
return (globalThis as any).__plugins[name];
}
// auth-plugin.ts
import "./plugin-registry";
declare module "./plugin-registry" {
interface PluginRegistry {
auth: { login(user: string, pass: string): Promise<string> };
}
}
Each plugin augments the registry, and getPlugin returns the correctly typed plugin.
Extending a generic library type (e.g., adding metadata to a state store):
import "zustand";
declare module "zustand" {
interface StoreMutators<S, A> {
withMeta: S & { meta: { lastUpdated: Date } };
}
}
Patching missing type declarations for an untyped package:
// types/legacy-lib.d.ts
declare module "legacy-lib" {
export function init(config: { apiKey: string }): void;
export function send(payload: unknown): Promise<{ status: number }>;
}
Best Practices
- Place augmentation files in a dedicated
types/directory and include it intsconfig.json'sincludeortypeRoots. - Always include
export {}in global augmentation files to ensure they are treated as modules. - Keep augmentations minimal — only add what your code actually uses, to avoid masking upstream type changes.
Common Pitfalls
- Forgetting
export {}in a file that usesdeclare global, causing it to be treated as a script and silently failing to augment anything. - Using
declare modulewith a relative path that does not exactly match the module's resolution path — the augmentation will create a new module instead of merging with the existing one.
Anti-Patterns
Augmenting types you do not control with non-optional properties. Adding a required property to a third-party interface means every existing usage of that interface now has a type error unless the property is provided. Always make augmented properties optional unless you are certain every construction site is under your control.
Placing augmentation files outside tsconfig.json's include paths. If the augmentation .d.ts file is not included in compilation, the augmentation silently does not apply. Always verify that your include, files, or typeRoots configuration picks up the augmentation directory.
Augmenting instead of wrapping when you need behavioral changes. Module augmentation only adds type-level declarations; it does not change runtime behavior. If you need to add runtime functionality to a library object, create a wrapper function or middleware rather than augmenting the type and hoping the runtime matches.
Using declare module for a path that does not match module resolution. A relative path augmentation like declare module "./base" must exactly match how TypeScript resolves that module. A mismatch (e.g., including or omitting the file extension, or using a different relative path) creates a new phantom module instead of merging with the intended one.
Scattering augmentations across feature modules. When augmentations live next to the feature code that uses them, it becomes difficult to audit what has been added to third-party types. Consolidate augmentations in a single types/ directory so they are easy to find, review, and maintain during library upgrades.
Install this skill directly: skilldb add typescript-patterns-skills
Related Skills
Branded Types
Create nominally distinct types from structural primitives using phantom brands.
Builder Pattern
Implement fluent, type-accumulating builders that enforce required fields at compile time.
Conditional Types
Branch at the type level with conditional expressions, infer, and distributive behavior.
Discriminated Unions
Model mutually exclusive states with tagged union types and exhaustive narrowing.
Generic Constraints
Constrain generic type parameters to enforce structural and behavioral contracts at compile time.
Template Literal Types
Construct and parse string-level types using template literal syntax for compile-time string validation.