Module Augmentation

Difficulty: Advanced

Question

What is module augmentation in TypeScript? How do you extend existing library types without modifying their source?

Answer

Module augmentation lets you add new declarations to existing modules without touching the original source code. It works because TypeScript supports declaration merging: when two declarations of the same name exist, they are merged.

Common use cases: 1. Adding properties to Express Request/Response 2. Extending third-party library types with plugin methods 3. Adding global utility types or variables 4. Patching incomplete or incorrect library types

Syntax: Use 'declare module "module-name"' to augment an external module. The augmentation must be in a module file (has import/export).

Code examples

Augmenting Express Types

// express-augment.d.ts
import { User } from './models/user';

// Augment Express Request to include user from auth middleware
declare module 'express-serve-static-core' {
  interface Request {
    user?: User;
    requestId: string;
    startTime: number;
  }
}

// Now in your middleware and routes:
import { Request, Response, NextFunction } from 'express';

function authMiddleware(req: Request, res: Response, next: NextFunction) {
  const token = req.headers.authorization;
  req.user = verifyToken(token);    // OK - user exists on Request
  req.requestId = crypto.randomUUID(); // OK
  req.startTime = Date.now();          // OK
  next();
}

function getProfile(req: Request, res: Response) {
  if (!req.user) return res.status(401).json({ error: 'Not authenticated' });
  res.json(req.user); // Fully typed!
}

Augmenting Express is one of the most common real-world uses. By extending the Request interface, your auth middleware and route handlers share type-safe user data.

Extending Third-Party Libraries

// Augmenting a date library with custom methods
declare module 'dayjs' {
  interface Dayjs {
    toBusinessDay(): Dayjs;
    isBusinessDay(): boolean;
  }
}

// Augmenting a state management library
declare module 'zustand' {
  interface StoreApi<T> {
    getServerState(): T;
  }
}

// Extending a UI library's theme
declare module '@mui/material/styles' {
  interface Theme {
    brand: {
      primary: string;
      secondary: string;
    };
  }
  interface ThemeOptions {
    brand?: {
      primary?: string;
      secondary?: string;
    };
  }
}

// Interface merging within your own code
interface EventMap {
  click: MouseEvent;
  keypress: KeyboardEvent;
}

// Later in another file - extends EventMap
interface EventMap {
  scroll: Event;
  resize: UIEvent;
}

// EventMap now has all four members

Module augmentation and interface merging allow you to add types to libraries without forking them. This is critical for typed middleware, plugins, and theme systems.

Namespace & Global Augmentation

// Adding to a namespace
// Original library declares:
// declare namespace Express {
//   interface Request { ... }
// }

// You augment it:
declare namespace Express {
  interface Request {
    session: {
      userId: string;
      role: 'admin' | 'user';
    };
  }
}

// Global augmentation (must be in a module file)
export {}; // Makes this a module

declare global {
  function sleep(ms: number): Promise<void>;

  interface Array<T> {
    toShuffled(): T[];
    unique(): T[];
  }
}

// Implementation (runtime)
Array.prototype.unique = function<T>(this: T[]): T[] {
  return [...new Set(this)];
};

// Now globally available with types
const nums = [1, 2, 2, 3, 3];
nums.unique(); // [1, 2, 3] - typed as number[]

Global augmentation uses 'declare global' inside a module file. It can extend built-in types like Array and add global functions. Use sparingly as it affects the entire project.

Key points

Concepts covered

Module Augmentation, Declaration Merging, Extending Libraries, Global Augmentation, Ambient Modules