Lesson 42 of 80
Declaration merging has one mechanism but two different targets — the global scope, and a specific module's own exports. Knowing which one you need, and writing it correctly, is what separates a clean library extension from a d.ts file full of any.
13 min read
Every option explains itself — why the right one is right, and why each wrong one is wrong. Miss one and it comes back later on a spaced-repetition schedule.
This topic is about judgment and practical tradeoffs more than a single right answer — a mock interview is the better way to test yourself on it.
Be interviewed on this →Hints without spoilers, and a plain explanation of why a wrong answer was wrong — answered only from this lesson. Free account, no card.
Sign up freeWhat interviewers actually ask about this, and what a strong answer has to cover.
A strong answer covers
Likely follow-ups
The declaration-merging lesson showed the mechanism — two interface declarations with the same name combine — using Express's Request as the example. This lesson is about applying that same mechanism deliberately, and there are really two different things you might be augmenting, which use slightly different syntax:
window, globalThis, or a global ambient type that isn't tied to any specific import.import, without touching the library's source.Mixing these up is the single most common mistake when writing your first augmentation .d.ts file.
// globals.d.ts
export {}; // makes this file a module, which scopes the `declare global` block correctly
declare global {
interface Window {
dataLayer: Record<string, unknown>[];
}
}
// anywhere in the app
window.dataLayer.push({ event: "page_view" });
window.dataLayer is a real runtime object a lot of analytics scripts (Google Tag Manager, GA4) expect you to push events onto — but TypeScript's built-in Window interface has no idea it exists. declare global { interface Window { ... } } merges your addition into TypeScript's own global Window interface, so every file in the project sees dataLayer as a real, typed property, without you editing a library's source or casting to any every time you touch it.
// styled-components.d.ts
import "styled-components";
declare module "styled-components" {
export interface DefaultTheme {
colors: {
primary: string;
background: string;
};
}
}
This is a different target: styled-components exports its own DefaultTheme interface, empty by default specifically so consumers can fill it in with their app's actual theme shape. declare module "styled-components" { ... } reopens that specific module's types — not the global scope — and merges your colors shape into it. Every styled-components API that uses DefaultTheme (the theme prop, useTheme(), the css helper) now knows your app's real theme shape, fully typed, with zero changes to the library itself.
declare global and declare module "specific-package" are not interchangeable — one merges into the ambient global scope, the other reopens one specific module's exports. Reaching for the wrong one is why an augmentation compiles but the new property never actually shows up where you expect it.DefaultTheme.colors with an incompatible shape across two augmentation files is a real conflict, the same restriction the declaration-merging lesson already covered with Express's Request — this isn't a new rule, just the same one applied to a different target..d.ts file physically lives matters less than whether it's included by tsconfig.json. A correctly-written augmentation that isn't inside the project's configured include paths (or isn't referenced from a file that is) silently doesn't merge at all — no error, just types that behave as if the file doesn't exist.styled-components's DefaultTheme and a number of testing-library global matcher interfaces are designed to be empty until your app's own .d.ts file fills them in. That emptiness is a deliberate extension point, not an oversight."How would you add a typed dataLayer to window, or give styled-components your app's real theme type?" tests the same underlying mechanism as the Express Request question from the declaration-merging lesson, but forces you to correctly choose between declare global and declare module "package-name" — which is exactly where people who've memorized one example without understanding the mechanism get stuck.