Topic 11.5
Globals, Namespaces and Module Augmentation
In one line
Some names live everywhere (window, process, a bundler constant). declare global, interface merging and module augmentation let you add to existing types safely, and namespaces are the old way to group code that modules replaced.
Think of it like this
A school notice board. Everyone in the school can read it (a global). If the principal wants to add a new section, "Sports day", she doesn't build a new board: she adds to the existing one, and from then on everybody sees the new section. TypeScript lets you do the same with types: the standard Window type is a notice board, and you can add a section called appVersion to it without touching TypeScript's own files.
Words you'll meet
New words in this topic, in plain English. Come back here whenever one feels fuzzy.
- Global
- A name any file can use without importing it, like
window. - Script file
- A file without top-level import or export: its declarations are global.
- Declaration merging
- Several declarations of one name (like interfaces) combine into one.
- declare global
- A block inside a module that adds declarations to the global scope.
- Module augmentation
- Adding members to the types of an existing module with
declare module "x". - Namespace
- A named group of values and types in one object-like container.
- Window
- The type of the browser's global object
window. - erasableSyntaxOnly
- A setting that bans TypeScript syntax that needs more than deleting types, such as namespaces and enums.
Step by step
01The problem: a property TypeScript doesn't know
A script tag in your page sets window.appVersion = "1.0". In the editor, TypeScript doesn't know Window has such a property, and says so (the first live example shows the real error).
Casting with (window as any).appVersion removes the checking. Better: teach Window about it.
02Add it with declare global
In a module file, wrap the interface in declare global. Interfaces merge, so this does not replace Window: it adds one member to the standard one. The var line adds a global variable.
export {}; // makes this file a module
declare global {
interface Window {
appVersion: string;
}
var DEBUG: boolean;
}03Or use a script .d.ts for bundler constants
If the file has no import/export, everything in it is already global, and you don't need declare global. Real run: this compiled and __APP_VERSION__ and window.analytics were known everywhere. Adding export {}; at the top made both disappear (TS2304 and TS2339).
declare const __APP_VERSION__: string;
interface Window {
analytics: { track(event: string): void };
}04Augment a package
The package shapes declares interface Options { size: number }. We want a color option from a plugin. Create src/shapes-augment.ts (a module, thanks to import "shapes").
Before the file existed, tsc 7.0.2 said src/main.ts(3,29): error TS2353: Object literal may only specify known properties, and 'color' does not exist in type 'Options'. After adding it, the project compiled with no errors.
import "shapes";
declare module "shapes" {
interface Options {
color?: string;
}
}05Why modules replaced namespaces
A namespace and a module both group names, but a module is a file the bundler can analyse and drop if unused, while namespaces build a runtime object. Today you use export in files. Know namespaces to read old code and .d.ts files, and know they conflict with erasableSyntaxOnly.
Try it yourself
- 1
Add a property
In the second example add
theme: "light" | "dark";toWindow, then writewindow.theme = "blue";. Predict the error before you look:TS2322, because"blue"is not one of the two strings. - 2
Merge a clash
In the third example, add a third
interface Box { width: string }. Predict: tsc complains that two declarations ofwidthhave different types (TS2717, subsequent property declarations must have the same type). - 3
Peek inside a namespace
In the fourth example, try
Geometry.circle(2) + Geometry.secret. A name that wasn't exported isn't visible. Addconst secret = 1;inside the namespace first, then see the error.
Code & diagrams
TypeScript lists Window & typeof globalThis because window is also globalThis. The code still runs (the browser lets you add properties), which is why the checker's complaint matters: it can't know you meant it.
Console
(nothing was printed)Expected console output
1.0The editor file is a module, so declare global is allowed. The interface merges with the browser's Window. The runtime values are still set by your own code.
Console
(nothing was printed)Expected console output
1.4.2 trueThe two Box declarations became one. Array<T> got a new method type, and we added the real method on Array.prototype ourselves. Changing built-in prototypes is a risky habit (a future JavaScript method might clash), so use this sparingly. The last line shows the merged Box requires both members.
Console
(nothing was printed)Expected console output
6
3Only exported members are visible outside. The second namespace merges with the function of the same name, giving a function that also has a property, the pattern that old JavaScript libraries (like jQuery's $) use.
Console
(nothing was printed)Expected console output
12.56 cm
Hi, AshaNode.js can run TypeScript files by deleting the types. A namespace needs real code to be generated, so with erasableSyntaxOnly on, tsc refuses it (as it does for enum and constructor parameter properties). Topic 12.5 covers running TypeScript.
Console
(nothing was printed)# src/globals.d.ts has NO import/export (a script):
declare const __APP_VERSION__: string;
interface Window { analytics: { track(event: string): void } }
$ tsc -p . # use.ts calls window.analytics.track and reads __APP_VERSION__
exit 0
# add export {}; as the first line -> it is now a module:
src/use.ts(1,13): error TS2304: Cannot find name '__APP_VERSION__'.
src/use.ts(2,8): error TS2339: Property 'analytics' does not exist on type 'Window & typeof globalThis'.
# the module keyword for a namespace:
$ cat a.ts
module Legacy { export const x = 1; }
$ tsc --noEmit --ignoreConfig a.ts
a.ts(1,8): error TS1540: A 'namespace' declaration should not be declared using the 'module' keyword. Please use the 'namespace' keyword instead.Break it on purpose
Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.
Break #1
Use declare global in a script
In a project where the file is a script (moduleDetection is auto, the default, and module is esnext with bundler), make src/a.ts contain only declare global { interface Window { x: number } }, with no export {}. (The live editor forces module detection, so it cannot show this one. Under nodenext with "type": "module" in package.json every .ts file counts as a module, so there is no error either.)
Break #2
Forget that it is only a promise
Declare appVersion on Window but never assign it, then print window.appVersion.length.
Myth vs fact
Myth
Namespaces are deprecated, never use them.
Fact
They aren't deprecated and .d.ts files use them heavily. You just shouldn't write them in new application code: modules do that job and are friendlier to bundlers and erasableSyntaxOnly.
Myth
declare global changes the runtime.
Fact
It only adds type information. Whatever sets the global at runtime is your job.
Myth
Augmenting a module can change existing members.
Fact
You can only add. Re-declaring an existing property with a different type is an error.
Pro corner
Extra depth for experienced readers. New to this? Skip it for now and come back later.
- ▸
Interface merging has an order rule for overloads (several signatures for one function): later declarations' overload lists come first. Library authors use this so a plugin's more specific overload wins. It is also why augmenting
ArrayorPromisecan change how overloads resolve in files far away: global augmentation is global. - ▸
Global augmentation leaks: every file in the program sees it, including your dependencies. Keep augmentations in one named file (
types/globals.d.ts) and check it is included byinclude. In a monorepo, make each package augment only what it owns. - ▸
TypeScript 7.0 does not allow the
modulekeyword for namespaces; migrate withnamespace. For.d.tsfiles that describe a UMD global (a script tag library),export as namespace Lib;declares the global name for a module-shaped file (UMD means one library file that works both as a module and as a global).
Remember this
- 1
The problem this solves: sometimes a name exists at run time that TypeScript did not create (
window.appVersionset by a script tag), or a library needs a new field from a plugin. These features let you tell the checker, instead of silencing it withany. (Recall from Topic 11.3:declareis a promise that something exists.) A global is a name any file can use without importing it, such aswindow,document,consoleorprocess. Their types come from declaration files that are scripts: files with no top-levelimport/export(Topic 11.1), so everything declared inside goes into the shared global scope.lib.dom.d.tsis one. If you want your own global (a__APP_VERSION__constant your bundler replaces), putdeclare const __APP_VERSION__: string;in a script.d.ts(real run: it worked; adding a singleexport {}to the same file made it a module and thenCannot find name '__APP_VERSION__'appeared). - 2
Declaration merging is a TypeScript rule: two
interfacedeclarations with the same name in the same scope become one interface with all the members. This is whyWindow,ArrayandHTMLElementTagNameMapcan be extended by anyone.typealiases do not merge (a second one is a duplicate-name error). The live example below merges twoBoxinterfaces. Members you add must not clash: the same property with a different type is an error. - 3
Inside a module file you can't write a plain
interface Window { … }, because that would create a new localWindow. To reach the global scope from a module use **declare global { … }**. Everything inside is added to the global declarations. A typical use:declare global { interface Window { appVersion: string } }after whichwindow.appVersiontype-checks. Without it you getTS2339: Property 'appVersion' does not exist on type 'Window & typeof globalThis'(real). Remember it is still a promise: you must setwindow.appVersionyourself at runtime. In adeclare globalblock,var(notlet/const) is how you declare a global variable that is also a property ofglobalThis. - 4
Module augmentation is the same idea for a package: in a module file write
declare module "shapes" { interface Options { color?: string } }and the package'sOptionsinterface gainscolor. In the real test project,draw({ size: 2, color: "red" })first failed withTS2353(excess property) and compiled after the augmentation file existed. The file must be a module (import "shapes";at the top is the usual way), and the augmentation can only *add* to interfaces and namespaces, not replace functions or change existing members. This is how plugins extend Express'sRequest, Vue's component options and other libraries. - 5
A namespace (
namespace Geometry { export const PI = 3.14 }) groups names under one object. It was TypeScript's answer to modules before ES modules existed. Now modules do that job, and namespaces are mostly found in old code and in.d.tsfiles that describe a library's global object. A namespace can merge with a function, class or enum of the same name (the live example adds a property to a function throughnamespace greet). Note: a namespace emits runtime code, an object plus a function wrapper, so it isn't erased. That is whyerasableSyntaxOnly(since 5.8, for tools like Node that only strip types) reportsTS1294on it, as it does forenumand parameter properties. - 6
Writing the keyword
modulefor a namespace (module Legacy { … }) is an error: both TypeScript 6.0.3 and 7.0.2 reportTS1540: A 'namespace' declaration should not be declared using the 'module' keyword. Please use the 'namespace' keyword instead.(real). In a new project, write ES modules and usedeclare globalor augmentation only for the few things that really are global.
Explain it without notes
How do you add a property to window in a TypeScript module?
What is declaration merging and which declarations merge?
What is module augmentation and what are its limits?
Why were namespaces replaced by modules, and when would you still meet them?
Practice
Write the declaration that lets window.theme = "dark" type-check but rejects "blue", in a file that has an export {}.
A plugin wants every Config interface from "app-core" to also have retries?: number. Write the augmentation file.
Trade-offs
- ↔
Augmenting globals is a convenient way to type environment facts (bundler constants, injected objects) but anyone in the program can see and depend on them. Keep each in one file with a comment saying who sets it.
- ↔
Augmenting a library's interface is the supported way to write plugins, but if the library renames or moves that interface, your augmentation silently stops applying and the new field becomes an error somewhere else.
Done when you can
I can explain script vs module files for global declarations.
I can write
declare globalto add toWindow.I can explain declaration merging, and which declarations merge.
I can write a module augmentation and say what it cannot do.
I can read a namespace and say why modules replaced them.