Skip to main content
Technology & EngineeringTypescript Patterns149 lines

Module Augmentation

Extend third-party and global type declarations without modifying source using module augmentation.

Quick Summary21 lines
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 lines
Paste into your CLAUDE.md or agent config

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

Common Pitfalls

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

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

Get CLI access →