TypeScript
Completed
-
Section 1: Getting Started
-
Section 2: Basic Types
-
Section 3: Functions and Objects
-
Section 4: Advanced Types
-
Section 5: Object-Oriented TypeScript
-
Section 6: Working with Modules
-
Section 7: Tooling and Practice
-
Section 8: Type-Level Programming
-
Section 9: TypeScript with Backends
-
Section 10: Testing Typed Code
-
Section 11: Data Validation with Types
-
Section 12: Practical Projects
-
Section 13: Compiler Internals
-
Section 14: Configuration Deep Dive
-
Section 15: Enums, Symbols, and Special Types
-
Section 16: Working with Async Code
-
Section 17: TypeScript and the DOM
-
Section 18: Advanced Generics Practice
-
Section 19: Working with Third-Party Types
-
Section 20: Monorepo and Large-Scale Practices
-
Section 21: Common Pitfalls and Best Practices
-
Section 22: Interview Practice
-
Section 23: Handbook: Narrowing In Depth
-
Section 24: Handbook: Object Types In Depth
-
Section 25: Handbook: Classes In Depth
-
Section 26: Handbook: Modules In Depth
-
Section 27: Handbook: Declaration Files In Depth
-
Section 28: JSX and Namespaces
-
Section 29: Compiler Configuration Reference
-
Section 30: More Practice Exercises
-
Section 31: Handbook: Everyday Types Deep Dive
-
Section 32: Utility Types Full Reference
-
Section 33: Decorators Reference
-
Section 34: Mixins and Advanced OOP Patterns
-
Section 35: Iterators and Generators Typing
-
Section 36: More Type-Level Programming Practice
-
Section 37: TypeScript Ecosystem Tools
-
Section 38: TypeScript with Testing Frameworks
-
Section 39: TypeScript for Library Authors
-
Section 40: More Interview Practice
-
Section 41: Handbook: Functions In Depth
-
Section 42: Handbook: Type Manipulation Deep Dive
-
Section 43: More Real-World Patterns
-
Section 44: TypeScript Release Notes Highlights
-
Section 45: Final Practice and Review
115: Module Augmentation
You've probably run into this scenario: you're using a popular third-party library, and you need to attach some custom data to one of its objects. The most classic example I see is with Express.js. You write an authentication middleware that verifies a JWT and then attaches the user's profile to the request object so that every subsequent route handler knows who is making the call.
import { Request, Response, NextFunction } from 'express';
interface User {
id: string;
email: string;
}
const authMiddleware = (req: Request, res: Response, next: NextFunction) => {
// Imagine we validated the token here...
const user: User = { id: 'usr_123', email: 'dev@example.com' };
// ❌ TypeScript Error: Property 'user' does not exist on type 'Request'.
req.user = user;
next();
};
The frustration of read-only third-party types
This is a maddening moment. You know the code works at runtime—JavaScript doesn't care if you add a property to an object on the fly. But TypeScript is looking at the Request interface defined inside @types/express and saying, "I don't see a user property here, so I can't let you do this."
You can't just go into your node_modules folder and edit the type definition because your changes will be wiped out the next time you run npm install. You might be tempted to use a type cast like (req as any).user = user, but that's basically waving a white flag. You lose all the type safety we're actually using TypeScript for in the first place.
Merging our own types into the module
This is where Module Augmentation comes in. In TypeScript, interfaces are "open," meaning if you declare an interface with the same name in the same module, TypeScript doesn't overwrite the old one—it merges them together. To do this for an external library, we use the declare module syntax.
Create a file—usually something like express.d.ts in a @types folder—and add this:
import { User } from './models/User';
declare module 'express' {
interface Request {
user?: User;
}
}
Wait, what just happened? By wrapping the Request interface inside declare module 'express', we told the compiler: "Hey, I know the express module already exists, but I want to add something to its existing Request interface."
Now, when you go back to your middleware, the error is gone. TypeScript now sees Request as the original Express request plus your optional user property. It's a seamless merge.
The "Import" Trap
Here is a nuance that trips up almost everyone: the difference between an ambient declaration and a module augmentation.
If your .d.ts file contains only declare module 'express' { ... } without any top-level import or export statements, TypeScript treats the file as a global script. But the moment you add an import at the top (like importing the User type), the file becomes a module itself. In a module, declare module 'express' specifically targets the existing external module for augmentation.
If you forget the import, you might accidentally create a new module named 'express' that replaces the original one entirely, which will break every single Express type in your project. If your types suddenly vanish, check for those top-level imports.
📋 Practical Task
Augmenting the Global Window Object for Analytics
You are integrating a third-party analytics script (like Google Analytics or Mixpanel) that attaches a global trackEvent function to the browser's window object. Currently, TypeScript is complaining that trackEvent does not exist on Window.
Your Goal: Create a module augmentation to let TypeScript know that the global window object has a trackEvent method that takes a string (the event name) and an optional object of properties.
Requirements:
- Create a declaration file (or simulate one in your environment).
- Augment the global
Windowinterface. - Define
trackEventas a function:(name: string, properties?: Record<string, any>) => void. - Write a small snippet of code that calls
window.trackEvent('button_click', { color: 'blue' })without causing a compiler error.
There are no comments for now.