Right, so you’ve met your first .d.ts file and you’re feeling pretty good. You can describe the shape of a library or your own code. But then you stumble into a codebase and see something like this:

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

Your first thought is, “Wait, declare global? What is this, some kind of incantation?” And your second thought is, “Why would I ever need this?” Buckle up, because we’re about to answer both. This is where we move from describing types to augmenting them—officially, and globally.

The core idea is simple: sometimes, the type definitions you get from @types/ packages or that TypeScript provides out-of-the-box aren’t enough. Some library adds a property to the global window object. Your favorite testing framework adds a global jest object. You, in a moment of weakness, decide to attach a helper function to Array.prototype (we’ve all been there, no judgment). Global augmentation is how you tell TypeScript, “Trust me, this thing exists now. Add it to your understanding of the world.”

The Two Flavors of Augmentation

There are two distinct scenarios, and confusing them is a classic rookie mistake. They look similar but behave very differently.

Module Augmentation is when you want to add to something that lives inside another module. This is the most common kind. You’re extending an interface that was exported from some @types/ package.

Global Augmentation is when you want to add to something that exists in the global scope, like window, Document, or Array. This is less common but incredibly powerful when you need it.

Here’s the crucial, can’t-stress-this-enough rule: TypeScript will only allow a declare global block inside a module file. Wait, what? A module file is any file that has an import or export statement. If your file is not a module (i.e., it’s a script), the global scope is already its playground; you can just write interface Window { ... } directly. The declare global block is specifically for when you’re inside a module context but need to “break out” to declare something on the global scope.

Augmenting a Module

Let’s say you’re using express and you want to add a custom property to the Request object. The @types/express package authors were wise and made the Request interface open, meaning it’s meant to be extended. Here’s how you do it:

// express-augmentation.ts
import { Request } from 'express';

// This declares that we're augmenting the Express namespace.
declare module 'express' {
  interface Request {
    // Our custom property is now part of the official Request type.
    user?: {
      id: string;
      isPremium: boolean;
    };
  }
}

// Now, in any middleware after this augmentation is loaded, `req.user` is perfectly valid.
export const authMiddleware = (req: Request, res: Response, next: NextFunction) => {
  const userId = req.headers['x-user-id'];
  if (userId) {
    req.user = { // ✅ TypeScript knows this property exists!
      id: userId as string,
      isPremium: true // let's be optimistic
    };
  }
  next();
};

The key here is declare module 'express'. We’re not redeclaring the whole module; we’re tapping into it and saying, “Add this new stuff to your existing interfaces.” It’s beautifully non-invasive.

Augmenting the Global Scope

Now for the weird one. Let’s say you’re using a legacy script that slaps a value onto window. Or you’re using a testing framework that creates a global jest object.

// global-augmentation.ts
// This file MUST be a module. Let's export something trivial to ensure it is.
export {}; // <-- This line is the magic key.

// Now we can break out into the global scope.
declare global {
  interface Window {
    __MY_APP_STATE__: {
      debugMode: boolean;
    };
  }

  // You can also declare new global variables, not just extend interfaces.
  const __VERSION__: string; // This will be a var in the global scope
}

// Later in your module code:
function initDebug() {
  if (window.__MY_APP_STATE__.debugMode) { // ✅ Fully typed!
    console.log("Debug mode engaged!");
  }
}

See that export {};? That’s what makes this file a module. Without it, the declare global block would be an error. It’s TypeScript’s way of making sure you really intend to do a global augmentation and didn’t just stumble into it by accident.

Pitfalls and the “Do Not Do This” List

  1. File Scope is Crucial: If you put a global augmentation in a file that’s not a module (no import/export), it will error. If you put a module augmentation in a global script file, it won’t work. Know your context.
  2. Don’t Shadow, Augment: The biggest mistake is trying to redeclare an entire module or global interface. You’ll get duplicate identifier errors. You must only extend the existing one. If the original interface wasn’t written to be extended (i.e., it wasn’t in a global scope or declared with an interface), you’re out of luck. This is why type aliases can’t be augmented—they’re closed.
  3. Order Matters: Your augmentations need to be loaded by TypeScript after the original definitions. Usually, this just means ensuring your augmentation file is included in your tsconfig.json files or include array. If you get errors about the base interface not existing, you’ve likely got an ordering problem.
  4. Use This Power Wisely: Global augmentation is a sledgehammer. It affects your entire project. Use it for things that are truly global, like polyfills or application-wide state containers. For everything else, prefer module augmentation or, even better, just passing values around as function parameters. You’ll thank yourself later.