Skip to Content
Course content

115: Module Augmentation

Click on the "Edit" button in the top corner of the screen to edit your slide content.

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 Window interface.
  • Define trackEvent as 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.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.