In TypeScript, how do you add a typed property to the DOM `Window` interface from inside a module file, and what does the `declare global` block do there?
answer
- module scope hides your declaration
- the file needs an escape hatch
- reopen the global declaration space
- export {} to make it a module
- var, not const, for globalThis
basics
~20 sWrap the declaration in a declare global block inside a module file: it reopens the global declaration space so interface Window { ... } merges with the DOM's Window instead of creating a new module-local interface.
solid answer
~50 sInside a module — any file with a top-level `import` or `export` — every declaration is module-scoped, so a bare `interface Window { appVersion: string }` there just defines a brand-new local interface and `window.appVersion` still errors. `declare global { interface Window { appVersion: string } }` reopens the global scope, and the declaration merges with the `Window` interface from the DOM lib. For a bare global identifier rather than a property of `window`, declare it with `var` inside the same block — `declare global { var appVersion: string }` — because `var` is what contributes to the `globalThis` type. The block is only legal in a module or an ambient module declaration; in a plain script file there is nothing to reopen, since a top-level `interface Window` is already global and merges on its own.
code
typescript · 12 linesexport {}; // makes this file a module
declare global {
interface Window {
appVersion: string;
}
var buildId: string;
}
window.appVersion = '1.4.0';
console.log(globalThis.buildId);go deeper
Recall the shape of the fix: a declare global block containing interface Window { ... }, in a file that has an import or export {}.
Explain the script-versus-module rule that makes the block necessary, and distinguish merging into Window from declaring a var global that shows up on globalThis.
Show that you treat the declaration as an unverified claim: make members optional when a bootstrap may not have run, and keep the augmentation beside the code that assigns the global so provenance is traceable.
Own the policy question — global augmentations apply to the whole compilation and cannot be scoped per consumer, so decide which globals are sanctioned, who may declare them, and when a typed accessor is the better contract.
## The problem the block solves TypeScript classifies every file as either a **script** or a **module**. A file with at least one top-level `import` or `export` is a module, and everything declared in it is scoped to that module. A file with none is a script, and its top-level declarations live in the global declaration space. That distinction decides whether merging can reach the DOM's `Window`. In a script file, this is enough: ```ts // no imports/exports in this file -> it is a script interface Window { appVersion: string; } ``` The declaration lands in the global space and merges with the `Window` interface declared by the DOM library, exactly like any other same-name interface merge. In a module file the identical text does something quite different: it declares a private interface named `Window` that shadows nothing, merges with nothing, and leaves `window.appVersion` an error. This is one of the most common "why doesn't my declaration work" moments in TypeScript, and the answer is always "your file is a module". ## declare global `declare global { ... }` is the escape hatch. It reopens the global declaration space from inside a module: ```ts export {}; // makes this file a module declare global { interface Window { appVersion: string; } } window.appVersion = '1.4.0'; ``` The compiler is explicit about where the block may appear: it must be directly nested in a module or in an ambient module declaration. Write it in a script file and you get an error telling you exactly that — which sounds backwards until you remember that a script file is *already* global and needs no reopening. The `export {}` line is a no-op export whose only job is to make the file a module. If your file already imports or exports something real, you do not need it. ## Window versus globalThis There are two different things people mean by "a global". **A property of `window`.** Merge into the `Window` interface, as above. This is the right choice for browser-only values, and `globalThis.appVersion` will also type-check in a DOM-lib program because `globalThis` is typed by that same `Window` interface in browser lib configurations. **A bare global binding** you reference as `appVersion` with no receiver. Declare a variable in the global block, and declare it with `var`: ```ts export {}; declare global { var appVersion: string; } console.log(appVersion); console.log(globalThis.appVersion); ``` `var` matters. Only `var`-declared globals contribute a property to the `globalThis` type; `let` and `const` in the global scope model bindings that are not properties of the global object, mirroring the runtime rule, so `globalThis.appVersion` would not type-check if you had used `const`. Unlike an interface, a `var` declaration does not merge — declaring the same global name twice with `var` collides in the usual way, which is a feature when two packages fight over the same global. ## What you can reopen this way Anything in the global declaration space that merges: the built-in DOM interfaces (`Window`, `Document`, `HTMLElement`), a global `namespace`, or your own global interfaces. You are doing plain declaration merging; `declare global` only chooses *where* the declaration lands. You cannot merge into a global `type` alias — aliases are closed everywhere — and you cannot use the block to change the type of an existing member. If the DOM already declares a member with that name and a different type, you get the merge conflict error, not a silent override. ## The honesty problem The declaration is a claim, not an implementation. Types are erased: nothing in the emitted JavaScript creates `window.appVersion`, and nothing checks that whatever was supposed to set it actually ran. If the bootstrap script is missing, the compiler still lets you read the property and you get `undefined` at runtime with no warning. Two habits keep that honest. First, declare the member optional (`appVersion?: string`) whenever the value is set by something that might not have run, so callers are forced to handle its absence. Second, keep the augmentation next to the code that actually assigns the global, so a reader who finds the property in an editor tooltip can find its owner. Because the augmentation applies to the entire compilation — every file sees it whether or not it imports yours — a stray declaration in an unrelated corner of the codebase is genuinely hard to trace.
- Why does the same `interface Window { ... }` declaration work at the top level of one file but not another?Because one file is a script and the other is a module. A file with any top-level `import` or `export` is a module, so its declarations are module-scoped and merge with nothing global; a file with none is a script whose top-level declarations already live in the global space. In the module, wrap the declaration in `declare global`.
- You declared `window.featureFlags` and it type-checks, but it is `undefined` at run time. What went wrong?Nothing in the type layer — the declaration is a promise, not an implementation. Types are erased, so the augmentation emits no code and never verifies that the script assigning the global ran. Fix the load order or the bootstrap, and make the member optional so the compiler forces callers to handle the absent case.
- Can you use `declare global` to change the type of a property the DOM lib already declares?No. The block still performs ordinary interface merging, so a same-named property with a different type is a merge conflict error rather than an override. You can only add members. If you need a different type for an existing one, model it locally — for example, a narrow accessor function that reads the value and returns the type you want.
saying these in an interview costs you the question
- Thinks a top-level interface always merges globally regardless of file
- Omits export {} and wonders why the declaration is ignored
- Declares a global with const and expects globalThis to see it
- Believes declaring window.x makes the property exist at runtime
- Says declare global can override an existing DOM member's type