Module Augmentation & Global Types
🧩 Module Augmentation & Global Types
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:
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
Before vs After Augmentation
Without Augmentation
With Augmentation
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:
🌐 Global Augmentation
Extending the global scope itself (Window in a browser, globalThis in Node) uses declare global instead of declare module:
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:
💻 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.
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.
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.
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.