Command Palette

Search for a command to run...

Hectal
PHASE 11Intermediate ~22 min· topic 5 of 6

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.

src/globals.tswhole filets
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).

src/globals.d.tswhole filets
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.

src/shapes-augment.tswhole filets
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.

Why modules replaced namespacesdiagram
Rendering diagram…

Try it yourself

  1. 1

    Add a property

    In the second example add theme: "light" | "dark"; to Window, then write window.theme = "blue";. Predict the error before you look: TS2322, because "blue" is not one of the two strings.

  2. 2

    Merge a clash

    In the third example, add a third interface Box { width: string }. Predict: tsc complains that two declarations of width have different types (TS2717, subsequent property declarations must have the same type).

  3. 3

    Peek inside a namespace

    In the fourth example, try Geometry.circle(2) + Geometry.secret. A name that wasn't exported isn't visible. Add const secret = 1; inside the namespace first, then see the error.

Code & diagrams

Window doesn't know your property

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.0
declare global adds to Window and the global scope

The 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 true
Interfaces merge, even the built-in Array

The 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
3
Namespaces group code, and merge with functions

Only 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, Asha
Namespaces are not erasable syntax TypeScript 5.8+

Node.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)
Project: script vs module .d.ts, and augmentation (real tsc 7.0.2)bash
# 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.)

terminal
$ tsc -p .
── what you'll see ──
src/a.ts(1,9): error TS2669: Augmentations for the global scope can only be directly nested in external modules or ambient module declarations.

Break #2

Forget that it is only a promise

Declare appVersion on Window but never assign it, then print window.appVersion.length.

terminal
$ Console
── what you'll see ──
Uncaught TypeError: Cannot read properties of undefined (reading '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 Array or Promise can 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 by include. In a monorepo, make each package augment only what it owns.

  • ▸

    TypeScript 7.0 does not allow the module keyword for namespaces; migrate with namespace. For .d.ts files 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. 1

    The problem this solves: sometimes a name exists at run time that TypeScript did not create (window.appVersion set 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 with any. (Recall from Topic 11.3: declare is a promise that something exists.) A global is a name any file can use without importing it, such as window, document, console or process. Their types come from declaration files that are scripts: files with no top-level import/export (Topic 11.1), so everything declared inside goes into the shared global scope. lib.dom.d.ts is one. If you want your own global (a __APP_VERSION__ constant your bundler replaces), put declare const __APP_VERSION__: string; in a script .d.ts (real run: it worked; adding a single export {} to the same file made it a module and then Cannot find name '__APP_VERSION__' appeared).

  2. 2

    Declaration merging is a TypeScript rule: two interface declarations with the same name in the same scope become one interface with all the members. This is why Window, Array and HTMLElementTagNameMap can be extended by anyone. type aliases do not merge (a second one is a duplicate-name error). The live example below merges two Box interfaces. Members you add must not clash: the same property with a different type is an error.

  3. 3

    Inside a module file you can't write a plain interface Window { … }, because that would create a new local Window. 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 which window.appVersion type-checks. Without it you get TS2339: Property 'appVersion' does not exist on type 'Window & typeof globalThis' (real). Remember it is still a promise: you must set window.appVersion yourself at runtime. In a declare global block, var (not let/const) is how you declare a global variable that is also a property of globalThis.

  4. 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's Options interface gains color. In the real test project, draw({ size: 2, color: "red" }) first failed with TS2353 (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's Request, Vue's component options and other libraries.

  5. 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.ts files 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 through namespace greet). Note: a namespace emits runtime code, an object plus a function wrapper, so it isn't erased. That is why erasableSyntaxOnly (since 5.8, for tools like Node that only strip types) reports TS1294 on it, as it does for enum and parameter properties.

  6. 6

    Writing the keyword module for a namespace (module Legacy { … }) is an error: both TypeScript 6.0.3 and 7.0.2 report TS1540: 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 use declare global or augmentation only for the few things that really are global.

Explain it without notes

01

How do you add a property to window in a TypeScript module?

02

What is declaration merging and which declarations merge?

03

What is module augmentation and what are its limits?

04

Why were namespaces replaced by modules, and when would you still meet them?

Practice

01

Write the declaration that lets window.theme = "dark" type-check but rejects "blue", in a file that has an export {}.

02

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 global to add to Window.

  • 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.