Module Augmentation & Global Types

Advanced TypeScript
Course 4 ยท Chapter 7 ยท Module Augmentation & Global Types

๐Ÿงฉ Module Augmentation & Global Types

Every type built so far in this course lived in code you own. Sometimes the type that needs changing belongs to a library โ€” Express's Request, the global Window, even Array.prototype itself. This chapter covers the mechanism that makes that safe: declaration merging, module augmentation, and where a type-safe polyfill's type declaration ends and its actual implementation must begin.

The Foundational Mechanic: Declaration Merging

Two interface declarations with the same name don't conflict โ€” they merge into one interface with every member from both. This is the one rule everything else in this chapter builds on:

interface User {
  id: string;
}

interface User {
  email: string;
}

// TypeScript merges both declarations automatically:
const user: User = { id: "1", email: "a@b.com" };  // โœ… both fields required

// type User = { id: string };  // โŒ `type` aliases do NOT merge โ€” this would be a duplicate-name error

type aliases are single, fixed declarations โ€” only interface (and namespace) support this merging behavior, which is why library type definitions almost always use interface for anything meant to be extensible.

๐Ÿ“ฆ Augmenting a Third-Party Module

The most common real-world use: adding req.user to Express's Request type after an authentication middleware attaches it โ€” a gap left open by the Authentication & Session Security and Express courses' JWT middleware pattern:

Augmenting Express's Request

// types/express/index.d.ts
import "express";

interface AuthUser {
  id: string;
  role: "admin" | "member";
}

declare module "express" {
  interface Request {
    user?: AuthUser;
  }
}

Before vs After Augmentation

Without Augmentation
app.get("/profile", (req, res) => {
  console.log(req.user);
  // โŒ Property 'user' does not exist on type 'Request'
});
With Augmentation
app.get("/profile", (req, res) => {
  console.log(req.user?.role);
  // โœ… req.user is typed as AuthUser | undefined everywhere
});

import "express";

Required at the top of the augmentation file โ€” without a real import/export, TypeScript treats the file as a global script, not a module augmentation.

Merges Into Every Import

Once declared, Request's extra field is visible anywhere Express's types are imported โ€” no per-file re-declaration needed.

Augmenting Your Own Modules Across Files

The same merging mechanic works for your own large interfaces โ€” useful for splitting a plugin system's config across files without one giant file:

// core.ts
export interface PluginConfig {
  name: string;
}

// analytics-plugin.ts
import { PluginConfig } from "./core";

declare module "./core" {
  interface PluginConfig {
    analyticsEnabled?: boolean;
  }
}

๐ŸŒ Global Augmentation

Extending the global scope itself (Window in a browser, globalThis in Node) uses declare global instead of declare module:

// global.d.ts
export {};  // makes this file a module so `declare global` is allowed

declare global {
  interface Window {
    myAppVersion: string;
  }
}

// anywhere in the browser codebase:
console.log(window.myAppVersion);  // โœ… typed, no cast needed

Type-Safe Polyfills

Declaring a method exists on Array.prototype only tells the compiler about it โ€” it does not make the method actually exist at runtime. The type declaration and the real implementation are two separate, equally necessary steps:

// 1. The type declaration โ€” describes the shape, changes nothing at runtime
declare global {
  interface Array<T> {
    groupBy<K extends PropertyKey>(keyFn: (item: T) => K): Record<K, T[]>;
  }
}

// 2. The actual polyfill โ€” this is what makes it real at runtime
if (!Array.prototype.groupBy) {
  Array.prototype.groupBy = function(keyFn) {
    const result = {} as any;
    for (const item of this) {
      const key = keyFn(item);
      (result[key] ??= []).push(item);
    }
    return result;
  };
}

[1, 2, 3, 4].groupBy((n) => (n % 2 === 0 ? "even" : "odd"));
// { odd: [1, 3], even: [2, 4] } โ€” works because BOTH steps happened

๐Ÿ’ป Coding Challenges

Challenge 1: Merge Two Interfaces

Declare an interface Config in one code block with a port: number field, then declare a second interface Config adding a host: string field. Show that a single object literal must satisfy both fields at once.

Goal: Practice the core declaration-merging mechanic before applying it to a real module.

โ†’ Solution

Challenge 2: Augment Express's Request

Write a declare module "express" augmentation adding a requestId: string field to Request (simulating a correlation-ID middleware from the Logging & Observability chapter), then write a route handler that reads req.requestId without a type error.

Goal: Practice the real-world module augmentation pattern end-to-end.

โ†’ Solution

Challenge 3: Type and Implement a Polyfill

Add a type declaration for Array.prototype.last(): T | undefined via declare global, then write the actual runtime implementation that makes it work, and call it on a real array.

Goal: Practice keeping the type declaration and runtime implementation in sync, as this chapter's gotcha warns is easy to forget.

โ†’ Solution

โš ๏ธ Gotcha: A Type Declaration Is Not an Implementation

Writing declare global { interface Array<T> { groupBy... } } and stopping there compiles cleanly and then crashes at runtime with "array.groupBy is not a function" โ€” the declaration only tells the compiler what to expect, it does nothing to make the method exist. Also watch the build config: an augmentation `.d.ts` file that isn't included by your tsconfig.json's include pattern (or isn't imported/referenced from anywhere) is silently ignored โ€” the augmentation simply never applies, with no error to point at why.

๐ŸŽฏ What's Next

With the type system extended safely to code outside your control, the next chapter turns inward: Performance & Type Checker Optimization โ€” understanding why some of the patterns from this course can make tsc itself slow, and how to keep type inference fast on a large codebase.