Skip to content
Skip to content
DocsTypeScript ExpertexpertModule Augmentation
Chapter 10 of 10·expert·8 min read

Module Augmentation

Bổ Sung Module

Extend third-party library types without forking the source

Hover or tap any paragraph to see Vietnamese translation

Declaration Merging vs Module Augmentation

Declaration merging occurs when TypeScript combines multiple declarations with the same name in the same file. Module augmentation extends a module defined elsewhere — without forking its source or duplicating its types.

merging-vs-augmentation.ts
1// Declaration merging (same file — two interfaces merge)2interface Window {3  myProp: string;4}5interface Window {6  myOtherProp: number;7}8// Result: Window has both myProp and myOtherProp910// Module augmentation (separate file — extends an external module)11import "express";   // <-- This import makes the file a MODULE1213declare module "express" {14  interface Request {15    user?: AuthenticatedUser;  // merged into Express's Request16  }17}
Info
The critical distinction: the augmenting file must be a module file — it needs at least one import or export. A file without either is a script, not a module, and declare module inside it behaves differently.

Augmenting a Library Interface: Express Request

The most common use case is adding custom properties to Express's Request object after middleware has attached them. Without augmentation TypeScript errors on every req.user access.

express-augmentation.d.ts
1// src/types/express.d.ts2import "express";34// Augment the SOURCE package, not the re-exporter "express"5declare module "express-serve-static-core" {6  interface Request {7    user?: {8      id: string;9      email: string;10      roles: string[];11    };12    requestId: string;13    startTime: number;14  }15}1617// Tip: find the right module with Go to Definition on Request in your IDE
express-middleware.ts
1// src/middleware/auth.ts2import type { Request, Response, NextFunction } from "express";3import jwt from "jsonwebtoken";45export function authenticate(6  req: Request,7  _res: Response,8  next: NextFunction9): void {10  const token = req.headers.authorization?.split(" ")[1];11  if (!token) return next();12

Adding to Global Types with declare global

To extend the global scope — Window, globalThis, or standalone global variables — from a module file, wrap the declarations in declare global { }. This is the only way to do it from a file that already has imports or exports.

global.d.ts
1// src/types/global.d.ts2export {};  // makes the file a module — required for declare global34declare global {5  interface Window {6    // Analytics injected via third-party script tag7    analytics: {8      track(event: string, props?: Record<string, unknown>): void;9      identify(userId: string, traits?: Record<string, unknown>): void;10      page(name?: string): void;11    };12    __APP_VERSION__: string;
env.d.ts
1// src/types/env.d.ts — type process.env strictly2export {};34declare global {5  namespace NodeJS {6    interface ProcessEnv {7      NODE_ENV: "development" | "test" | "production";8      DATABASE_URL: string;9      JWT_SECRET: string;10      PORT?: string;11      REDIS_URL?: string;12      // Any key not listed here is now a type error13    }14  }15}1617// Usage — fully typed18const env = process.env.NODE_ENV;  // "development" | "test" | "production"19// process.env.UNKNOWN_KEY;        // Error: Property does not exist

Augmenting File Modules: CSS, SVG, JSON

TypeScript can import non-JavaScript files when you declare an ambient module for that extension. This enables fully typed imports of .css, .svg, and image files.

assets.d.ts
1// src/types/assets.d.ts2export {};34// CSS Modules — each class name maps to a string5declare module "*.module.css" {6  const classes: { readonly [key: string]: string };7  export default classes;8}910declare module "*.module.scss" {11  const classes: { readonly [key: string]: string };12  export default classes;
typed-json.d.ts
1// Typed JSON config — narrow the inferred shape for a specific pattern2declare module "*/config.json" {3  const config: {4    readonly apiUrl: string;5    readonly version: string;6    readonly features: Record<string, boolean>;7  };8  export default config;9}1011// Usage — fully typed, no 'any'12import config from "../config.json";13console.log(config.apiUrl);              // string14console.log(config.features["darkMode"]); // boolean

Pitfalls to Avoid

The Augmenting File Must Be a Module

If the file has no import or export, TypeScript treats it as a script. Declarations pollute the global scope unexpectedly and declare module may not merge correctly.

pitfall-module-vs-script.ts
1// WRONG — no imports or exports: TypeScript treats this as a SCRIPT2declare module "express-serve-static-core" {3  interface Request {4    user?: AuthUser;  // may not merge correctly5  }6}78// CORRECT — the empty export makes this a MODULE9export {};1011declare module "express-serve-static-core" {12  interface Request {13    user?: AuthUser;  // merges correctly into Express's Request14  }15}

Augment the Source, Not the Re-exporter

Many libraries re-export types from internal sub-packages. Augmenting the top-level package may silently fail if the interface actually lives in a different module.

pitfall-reexport.ts
1// WRONG — augmenting the re-exporter (may not merge)2declare module "express" {3  interface Request { user?: AuthUser; }4}56// CORRECT — augment where the interface is actually declared7declare module "express-serve-static-core" {8  interface Request { user?: AuthUser; }9}1011// Rule of thumb: use "Go to Definition" in your IDE on the type you want12// to augment to find the actual declaration's source module path

Practical Examples

NextAuth Session

next-auth.d.ts
1// types/next-auth.d.ts2import "next-auth";3import type { DefaultSession } from "next-auth";45declare module "next-auth" {6  interface Session {7    user: {8      id: string;9      role: "admin" | "user" | "moderator";10      organizationId: string;11    } & DefaultSession["user"];  // preserves name, email, image12  }

Fastify Decorators

fastify.d.ts
1// types/fastify.d.ts2import "fastify";3import type { PrismaClient } from "@prisma/client";45declare module "fastify" {6  interface FastifyRequest {7    user: {8      id: string;9      email: string;10      permissions: Set<string>;11    };12  }

Prisma — Adding Computed Fields

prisma-augmentation.ts
1// Prisma generates types from your schema — augment to add application-layer fields2import type { User } from "@prisma/client";34// Pattern: type alias with intersection — simpler than augmenting @prisma/client5export type UserWithFullName = User & {6  fullName: string;  // computed, not stored in DB7};89export function withFullName(user: User): UserWithFullName {10  return {11    ...user,12    fullName: `${user.firstName} ${user.lastName}`,

Summary

  • Declaration merging combines same-name declarations in the same file; module augmentation extends modules defined elsewhere
  • The augmenting file must be a module — add export {}; if it has no other imports or exports
  • Use declare global { } to extend Window, globalThis, and free variables from a module file
  • Augment the source module, not the re-exporter — use Go to Definition to find the correct path
  • Ambient module declarations for *.svg and *.css enable typed imports for non-JS assets
  • NextAuth, Fastify, and Prisma are all extended via module augmentation in real-world projects
  • Module augmentation cannot remove or change existing members — it can only add new ones
  • Built: 6/25/2026, 3:03:36 PM