TypeScript 60 🔷 Ambient Declarations
An ambient declaration is a description of something that exists at runtime but has no implementation in the current compilation. The word “ambient” means the declaration is a claim, not a definition. When TypeScript sees declare const VERSION: string, it accepts that a VERSION variable exists and trusts that the runtime provides it. The declaration introduces the name, the compiler uses it for checking, and no code is emitted for it. Ambient declarations are how TypeScript describes the global environment — the browser’s window, the Node.js process, the global variables a bundler injects, the libraries loaded from a CDN, the modules written in another language. They are the mechanism that brings the untyped world into the typed one, and they appear in every .d.ts file, every declare global block, every declare module augmentation, and every ambient namespace. This chapter covers the declare keyword in its full range, the ambient contexts, the interplay with modules and scripts, the common patterns for globals and libraries, and the pitfalls that make an ambient declaration a lie.
Key point: The declare keyword introduces an ambient declaration. It is used for variables, functions, classes, enums, namespaces, and modules. In a .d.ts file, the declarations are ambient by default, and the declare keyword is required for the ones that have a runtime representation. In a .ts file, the declare keyword is used to declare a global or an ambient name that is not imported. The ambient context is determined by whether the file is a module (has a top-level import or export) or a script (does not). A module’s ambient declarations are scoped to the module; a script’s are in the global scope. The declare module 'name' form declares a module’s shape, and the declare global block (in a module) adds to the global scope.
The declare keyword
The declare keyword has one meaning: “this exists at runtime, and I am describing its shape.” It is a claim, and the compiler trusts it.
declare const VERSION: string;
declare let counter: number;
declare function greet(name: string): string;
declare class Logger {
log(message: string): void;
}
declare enum Color {
Red,
Green,
Blue,
}
declare namespace MyLib {
function helper(): void;
}
Each declaration introduces a name in the ambient context. The compiler accepts the name and checks its uses. Nothing is emitted for the declarations, and the runtime is trusted to provide them.
Why declare is a claim. The compiler does not verify that the runtime provides the name. If the runtime does not, the code fails with a ReferenceError at runtime. The declare is a promise from the developer, and the promise can be wrong. The compiler checks the syntax and the types, not the existence.
Why declare is used in every .d.ts file. A .d.ts file has no runtime code, so every declaration in it is ambient. The declare keyword is required for the declarations that have a runtime representation: variables, functions, classes, enums, and namespaces. Interfaces and type aliases have no runtime representation, so they do not need it.
Why declare appears in .ts files. A .ts file can declare a global or an ambient name that is not imported. The declare keyword introduces the name, and the compiler accepts its use without an import.
// In a .ts file
declare const DEBUG: boolean;
export function log(message: string): void {
if (DEBUG) console.log(message);
}
The DEBUG is not imported and is not defined in the file. The declare tells the compiler it exists, and the bundler’s define option or the environment provides it at runtime.
Why declare is not used for type aliases. A type alias has no runtime representation. It is erased at compile time, so there is nothing to declare. The declare keyword is not needed and is not allowed for a type alias.
Why declare is not a runtime construct. The keyword is erased at compile time. It has no effect on the emitted JavaScript, and the runtime knows nothing about it. The declaration is a compile-time description, and the runtime provides the name independently.
Why the keyword is worth understanding in all its contexts. The declare keyword appears in the .d.ts files, the declare global blocks, the declare module augmentations, and the ambient namespaces. Each context has its own rules, and the keyword is the common thread. Knowing what it means in each context is the skill.
Ambient contexts
An ambient context is a place where declarations are ambient — where the declare keyword is either required or implicit. The context is determined by the file’s kind and the block it contains.
The .d.ts file. A .d.ts file is an ambient context. Every declaration in it is ambient, and the ones with a runtime representation need the declare keyword.
// globals.d.ts
declare const APP_VERSION: string;
declare function log(message: string): void;
interface Window {
myApp: { version: string };
}
The APP_VERSION and log are declared with declare. The Window interface is not, because it has no runtime representation.
The declare global block. A declare global block inside a module adds to the global scope. The block’s contents are ambient declarations in the global scope.
// In a module
export {};
declare global {
interface Window {
myApp: { version: string };
}
var myGlobal: string;
}
The export {} makes the file a module, and the declare global block adds to the global. The var is required for a global variable in a declare global block; the let and const are not allowed in this context.
The declare module block. A declare module 'name' block declares the shape of a module. It can be a declaration of a new module or an augmentation of an existing one.
// A new module declaration
declare module 'my-lib' {
export function request(url: string): Promise<Response>;
}
// An augmentation
import 'express';
declare module 'express' {
interface Request {
user?: { id: string };
}
}
The first declares a module that has no TypeScript source. The second augments the express module with a new property.
The ambient namespace. A declare namespace block groups ambient declarations. It is the classic form for a global library.
declare namespace MyLib {
interface Options {
timeout?: number;
}
function request(url: string, options?: Options): Promise<Response>;
}
The namespace groups the declarations, and the declare makes the namespace ambient. The consumer uses MyLib.request.
Why the context matters. The context determines whether the declarations are global or scoped, and whether the declare keyword is required. A .d.ts file that is a module has scoped declarations; a .d.ts file that is a script has global ones. The context is the first thing to determine when reading or writing a declaration file.
Why the export {} makes a file a module. A file with any top-level import or export is a module. The export {} is a way to make a file a module without exporting anything meaningful. The module scope is the default for new files, and the export {} is the marker.
Why a script’s declarations are global. A file without a top-level import or export is a script. Its declarations are in the global scope, and the declare keyword is not required for the top-level declarations in a .d.ts script, because the context is already ambient. The declare is still allowed and is often written for clarity.
Ambient variables and functions
An ambient variable or function declares a name that the runtime provides. The declaration describes the type, and the compiler checks the uses.
Ambient variables. The declare const, declare let, and declare var forms declare a variable.
declare const VERSION: string;
declare let counter: number;
declare var legacyGlobal: string;
The const is for a variable that does not change. The let is for one that does. The var is for the legacy form, and it is required in a declare global block.
Why the var in a declare global block. The declare global block’s contents are treated as if they were written in a script, where var is the only variable form. The let and const are not allowed because they are block-scoped, and the global scope is not a block. The var is the correct form.
Ambient functions. The declare function form declares a function.
declare function greet(name: string): string;
declare function fetch(url: string): Promise<Response>;
The function signature is declared, and the implementation is in the runtime. Overloads are declared with multiple declare function statements.
declare function parse(input: string): object;
declare function parse(input: object): string;
The two declarations are the overloads, and the implementation signature is not needed in the ambient context.
Why the overloads are declared. The ambient context has no implementation, so the overloads are the complete signature. The compiler uses them for checking, and the runtime is trusted to provide a function that matches.
Why the declare is not used for an interface. An interface has no runtime representation. It is declared without declare, and the compiler accepts it in the ambient context.
Why the declare keyword is sometimes omitted in a .d.ts script. The context is already ambient, so the declare is redundant. The style guides vary, and the declare is often written for consistency and clarity. The compiler accepts both.
Ambient classes and enums
An ambient class or enum declares a class or enum that the runtime provides. The declaration describes the shape, and the compiler checks the uses.
Ambient classes. The declare class form declares a class.
declare class Logger {
constructor(prefix: string);
log(message: string): void;
error(message: string): void;
static defaultPrefix: string;
}
The constructor, the methods, and the static members are declared. The implementation is in the runtime, and the compiler uses the declaration for checking.
Why the constructor is declared. The constructor signature is part of the class’s type. The new Logger(prefix) call is checked against it. The ambient class must declare the constructor with the parameters the runtime accepts.
Why the private members are not declared. The private members are not part of the public interface. The ambient class declares only the public surface, which is what the consumer uses. A private member declared in the ambient class would be visible to the consumer, which is not the intent.
Ambient enums. The declare enum form declares an enum.
declare enum Color {
Red,
Green,
Blue,
}
The enum’s members are declared, and the values are assigned by the runtime. The declare enum is a rare form, and the const enum has additional rules.
Why the declare enum is rare. An enum has a runtime representation, and a library that uses an enum ships the code. The ambient form is for a global enum that the runtime provides, which is uncommon. The declare enum appears in the type declarations for older libraries.
Why the ambient class is more common. A class with a constructor and methods is the shape of most library objects. The express types, the chalk types, and the ioredis types all declare classes. The ambient class is the standard form for an object with a constructor.
Why the ambient class does not have an implementation. The class exists at runtime, and its implementation is in the library’s JavaScript. The declaration describes the shape, and the compiler uses it. The runtime provides the behavior.
Ambient namespaces and modules
An ambient namespace or module groups declarations. The namespace is the classic form for a global library, and the module is the modern form for a library that is imported.
Ambient namespaces. The declare namespace form groups declarations under a name.
declare namespace MyLib {
interface Options {
timeout?: number;
}
function request(url: string, options?: Options): Promise<Response>;
class Client {
constructor(options?: Options);
get(url: string): Promise<Response>;
}
}
The namespace contains an interface, a function, and a class. The consumer uses MyLib.request, MyLib.Client, and MyLib.Options. The namespace is the classic form for a library that attaches itself to the global.
Why the namespace is nested for a deep hierarchy. A namespace can contain another namespace, which produces a deeper hierarchy. The dot notation extends: MyLib.Utils.format.
Ambient modules. The declare module 'name' form declares a module.
declare module 'my-lib' {
export interface Options {
timeout?: number;
}
export function request(url: string, options?: Options): Promise<Response>;
}
The export on each declaration makes it part of the module’s interface. The consumer imports the names and gets the types.
Why the ambient module is used for a library without types. A JavaScript library that has no .d.ts file is described with a declare module block. The block declares the module’s exports, and the consumer’s import is checked against them.
Why the ambient module can be a wildcard. The declare module '*.css' form declares a pattern for modules.
declare module '*.css' {
const content: string;
export default content;
}
The pattern matches any module ending in .css, and the default export is a string. The pattern is used for the asset imports that a bundler provides.
Why the wildcard is useful for assets. A bundler that imports a CSS file, an image, or a font produces a module that the bundler handles. The TypeScript compiler does not know the module’s shape, and the wildcard declaration provides it. The *.css, *.png, and *.svg patterns are common.
Why the ambient module is scoped to the module. The declare module 'my-lib' block declares the module’s shape, and the consumer’s import of 'my-lib' is checked against it. The block is not a global, and the declaration does not leak. The module is the unit, and the declaration describes it.
Why the import 'name' is required for augmentation. To augment an existing module, the file must import it first. The import makes the file a module, and the declare module 'name' block is an augmentation rather than a declaration. Without the import, the block would declare a new module, which is a different thing.
The declare global block
The declare global block is the mechanism for adding to the global scope from inside a module. It is used for browser globals, environment variables, and library globals.
export {};
declare global {
interface Window {
myApp: {
version: string;
config: Record<string, unknown>;
};
}
var myGlobal: string;
namespace NodeJS {
interface ProcessEnv {
DATABASE_URL: string;
PORT: string;
}
}
}
The block adds a myApp property to Window, a myGlobal variable to the global scope, and two properties to the NodeJS.ProcessEnv interface.
Why the export {} is required. The declare global block is allowed only in a module. The export {} makes the file a module, which is the prerequisite. Without it, the file is a script, and the block is an error.
Why the var is required for a global variable. The declare global block is treated as a script context, where only var is allowed. The let and const are block-scoped and are not allowed. The var is the form for a global.
Why the interface is not declared with declare. An interface has no runtime representation, so the declare keyword is not used. The interface is declared inside the block, and the compiler merges it with the existing Window interface.
Why the NodeJS.ProcessEnv augmentation is the environment variable pattern. The process.env object is typed as Record<string, string | undefined> by default. The augmentation declares the specific variables, which makes the code that reads them type-safe.
const dbUrl = process.env.DATABASE_URL; // typed as string
Without the augmentation, the type is string | undefined, and the code must check for undefined. With it, the type is string, and the check is not needed — the augmentation is a claim that the variable is set.
Why the Window augmentation is the browser global pattern. A library that adds a property to window is described with the Window interface augmentation. The myApp property is typed, and the code that reads it is checked.
Why the declare global block is a claim. The block declares that the runtime provides the names. If the runtime does not, the code fails at runtime. The block should be used only for the names the runtime actually provides, and the file should be included in the compilation.
Why the block is scoped to the compilation. The declare global block’s additions are visible in the files included in the compilation. A project that includes the file gets the additions; a project that does not, does not. The file is usually in the project’s src directory and included by the tsconfig.json.
The declare module block
The declare module block declares the shape of a module. It has two forms: the declaration of a new module, and the augmentation of an existing one.
The new module declaration. A module that has no TypeScript source is declared with the block.
declare module 'untyped-lib' {
export function request(url: string): Promise<Response>;
export interface Options {
timeout?: number;
}
}
The block declares the module’s exports, and the consumer’s import of 'untyped-lib' is checked against them. The block is the replacement for a .d.ts file that the library does not ship.
The augmentation. An existing module is augmented with the block, and the file must import the module first.
import 'express';
declare module 'express' {
interface Request {
user?: { id: string; name: string };
}
}
The import makes the file a module, and the block augments the express module’s Request interface. The interface merge applies, and the augmented Request has the user property.
Why the import is required for augmentation. Without the import, the file is a script, and the declare module 'express' block would declare a new module named express, which would override the real one. The import makes the file a module, and the block is an augmentation of the existing module.
Why the distinction matters. The two forms look identical, and the difference is the import. A missing import turns an augmentation into a declaration, which can silently override the library’s types. The error is subtle, and the fix is to add the import.
The wildcard declaration. The declare module '*.ext' form declares a pattern for a set of modules.
declare module '*.png' {
const content: string;
export default content;
}
declare module '*.svg' {
const content: string;
export default content;
}
The pattern matches any module ending in the extension, and the default export is a string. The pattern is used for the asset imports that a bundler provides.
Why the wildcard is useful for a bundler. A bundler that imports a CSS file, an image, or a font produces a module that the bundler handles. The TypeScript compiler does not know the module’s shape, and the wildcard declaration provides it. The *.css, *.png, and *.svg patterns are common.
Why the wildcard has limitations. The pattern matches the extension, and the declared type is the same for every file. A more specific type for a particular file requires a more specific declaration. The wildcard is a coarse mechanism, and it is the right tool for the assets that have a uniform shape.
Why the ambient module is not executed. The declare module block has no runtime code. The module it describes is provided by the runtime — the library’s JavaScript, the bundler’s asset handling. The block is a description, and the runtime provides the implementation.
Common patterns
The ambient declarations appear in a handful of patterns, and each is worth recognizing.
The global variable. A variable the runtime provides is declared with declare const or declare var. The pattern is used for the version strings, the build flags, and the globals a bundler injects.
declare const __DEV__: boolean;
declare const __VERSION__: string;
The global function. A function the runtime provides is declared with declare function. The pattern is used for the global helpers that predate modules.
declare function require(module: string): unknown;
declare function define(deps: string[], factory: () => void): void;
The global namespace. A library that attaches to the global is declared with declare namespace. The pattern is the classic form for jQuery, lodash, and moment.
declare namespace _ {
function map<T, U>(array: T[], fn: (value: T) => U): U[];
}
The module declaration. A library without types is declared with declare module. The pattern is the replacement for a missing .d.ts file.
declare module 'untyped-lib' {
export function request(url: string): Promise<Response>;
}
The module augmentation. A library is extended with the declare module block and an import. The pattern is used for the middleware properties, the plugin methods, and the new options.
import 'express';
declare module 'express' {
interface Request { user?: User; }
}
The global augmentation. The global scope is extended with the declare global block. The pattern is used for the browser globals, the environment variables, and the library globals.
export {};
declare global {
interface Window { myApp: App; }
}
The asset wildcard. A file type is declared with the declare module '*.ext' form. The pattern is used for the CSS, image, and font imports that a bundler handles.
declare module '*.css' {
const content: string;
export default content;
}
Why the patterns are worth recognizing. The patterns appear in the .d.ts files of every library, the global.d.ts of every project, and the type declarations of every framework. Reading them requires knowing the patterns, and writing them requires understanding which pattern applies.
Why the patterns should be used sparingly. Every ambient declaration is a claim about the runtime. A claim that is wrong causes a runtime failure that the compiler did not catch. The patterns should be used only for the names the runtime provides, and the declarations should be as precise as the runtime allows.
Why the patterns should be documented. A global variable declared in a global.d.ts is not visible in the code that uses it without a comment or a reference. The file’s purpose should be documented, and the source of the global — the bundler’s define, the environment, the library — should be noted. The documentation is what makes the claim reviewable.
Complete Example Session
// ============================================
// PART 1: AMBIENT VARIABLES
// ============================================
declare const APP_VERSION: string;
declare let counter: number;
declare var legacyGlobal: string;
console.log(APP_VERSION);
counter = 1;
legacyGlobal = 'x';
// ============================================
// PART 2: AMBIENT FUNCTIONS
// ============================================
declare function greet(name: string): string;
declare function parse(input: string): object;
declare function parse(input: object): string;
console.log(greet('Alice'));
// ============================================
// PART 3: AMBIENT CLASSES
// ============================================
declare class Logger {
constructor(prefix: string);
log(message: string): void;
error(message: string): void;
static defaultPrefix: string;
}
const logger = new Logger('[app]');
logger.log('started');
// ============================================
// PART 4: AMBIENT ENUMS
// ============================================
declare enum Color {
Red,
Green,
Blue,
}
const c: Color = Color.Red;
// ============================================
// PART 5: AMBIENT NAMESPACES
// ============================================
declare namespace MyLib {
interface Options {
timeout?: number;
}
function request(url: string, options?: Options): Promise<Response>;
class Client {
constructor(options?: Options);
get(url: string): Promise<Response>;
}
}
MyLib.request('/api');
const client = new MyLib.Client();
// ============================================
// PART 6: AMBIENT MODULES
// ============================================
declare module 'untyped-lib' {
export function request(url: string): Promise<Response>;
export interface Options {
timeout?: number;
}
}
import { request } from 'untyped-lib';
// ============================================
// PART 7: MODULE AUGMENTATION
// ============================================
import 'express';
declare module 'express' {
interface Request {
user?: { id: string; name: string };
}
}
// ============================================
// PART 8: GLOBAL AUGMENTATION
// ============================================
export {};
declare global {
interface Window {
myApp: {
version: string;
};
}
var myGlobal: string;
namespace NodeJS {
interface ProcessEnv {
DATABASE_URL: string;
PORT: string;
}
}
}
// ============================================
// PART 9: ASSET WILDCARDS
// ============================================
declare module '*.css' {
const content: string;
export default content;
}
declare module '*.png' {
const content: string;
export default content;
}
declare module '*.svg' {
const content: string;
export default content;
}
// ============================================
// PART 10: WHAT NOT TO DO
// ============================================
// Don't use let or const in a declare global block
// declare global { let x: number; } // ❌ use var
// Don't forget the import for a module augmentation
// import 'express'; // required
// Don't use the `declare` keyword for an interface
// interface User {} // no declare needed
// Don't declare a name the runtime does not provide
// declare const DOES_NOT_EXIST: string; // a lie
// Don't forget to include the .d.ts in the compilation
// The declarations are not applied.
// Don't put runtime code in an ambient context
// declare function f() { return 1; } // ❌ body not allowed
The ten parts cover the ambient variables, functions, classes, enums, namespaces, modules, augmentation, global augmentation, asset wildcards, and the anti-patterns.
Quick Reference
The declare Forms
| Form | Declares |
|---|---|
declare const/let/var | Variable |
declare function | Function |
declare class | Class |
declare enum | Enum |
declare namespace | Namespace |
declare module 'name' | Module |
declare global | Global (in a module) |
The Ambient Contexts
| Context | Where |
|---|---|
.d.ts file | Every declaration is ambient |
declare global | Global scope, from a module |
declare module | A named module |
| Ambient namespace | Global, grouped |
The Module vs Script Rule
| File | Top-level export | Scope |
|---|---|---|
| Module | Yes | Scoped to the file |
| Script | No | Global |
The declare global Requirements
| Requirement | Reason |
|---|---|
| The file must be a module | The block is module-only |
export {} | Makes the file a module |
var for globals | let and const are not allowed |
No declare on interfaces | Interfaces have no runtime |
Common Patterns
| Pattern | Form |
|---|---|
| Global variable | declare const X: T |
| Global function | declare function f(): T |
| Global library | declare namespace Lib {} |
| Untyped module | declare module 'name' {} |
| Module augmentation | import 'name'; declare module 'name' {} |
| Global augmentation | export {}; declare global {} |
| Asset wildcard | declare module '*.ext' {} |
The Asset Wildcards
| Extension | Type |
|---|---|
*.css | string |
*.png | string |
*.svg | string |
*.json | any (with resolveJsonModule) |
Best Practices
✅ Do This:
// Use declare for a runtime value
declare const VERSION: string; // ✅
// Use var in a declare global block
declare global { var myGlobal: string; } // ✅
// Use export {} to make the file a module
export {};
declare global { interface Window { myApp: App; } } // ✅
// Import before a module augmentation
import 'express';
declare module 'express' { interface Request { user?: User; } } // ✅
// Use a wildcard for asset imports
declare module '*.css' { const content: string; export default content; } // ✅
// Use the NodeJS.ProcessEnv augmentation for env vars
declare global { namespace NodeJS { interface ProcessEnv { PORT: string; } } } // ✅
// Document the source of the global
// The bundler's define provides __DEV__ // ✅
❌ Don’t Do This:
// Don't use let or const in a declare global block
declare global { let x: number; } // ❌ use var // ⚠️
// Don't forget the import for an augmentation
declare module 'express' { ... } // declares a new module // ⚠️
// Don't declare a name the runtime does not provide
declare const DOES_NOT_EXIST: string; // a lie // ⚠️
// Don't put runtime code in an ambient context
declare function f() { return 1; } // ❌ body not allowed // ⚠️
// Don't use declare for an interface
declare interface User {} // unnecessary // ⚠️
// Don't forget to include the .d.ts in the compilation
// The declarations are not applied. // ⚠️
// Don't use a broad wildcard without reason
declare module '*' { const x: any; export default x; } // dangerous // ⚠️
Common Pitfalls
| Pitfall | Problem | Solution |
|---|---|---|
let/const in declare global | Compile error | Use var |
| Missing import for augmentation | Overrides the module | Add the import |
declare on an interface | Unnecessary | Omit it |
| Body in an ambient function | Compile error | Signature only |
.d.ts not included | Declarations not applied | Check include |
| Broad wildcard | Overrides real modules | Use a specific extension |
| Claim without runtime | Runtime error | Declare only what exists |
Module file without export {} | Declarations are global | Add export {} |
Real-World Examples
1. Version global
declare const APP_VERSION: string;
2. Build flag
declare const __DEV__: boolean;
3. Global function
declare function require(module: string): unknown;
4. Global library
declare namespace _ {
function map<T, U>(array: T[], fn: (value: T) => U): U[];
}
5. Untyped module
declare module 'untyped-lib' {
export function request(url: string): Promise<Response>;
}
6. Express augmentation
import 'express';
declare module 'express' {
interface Request { user?: User; }
}
7. Window augmentation
export {};
declare global {
interface Window { myApp: App; }
}
8. Environment variables
export {};
declare global {
namespace NodeJS {
interface ProcessEnv { DATABASE_URL: string; PORT: string; }
}
}
9. CSS wildcard
declare module '*.css' {
const content: string;
export default content;
}
10. Image wildcard
declare module '*.png' {
const content: string;
export default content;
}
Visual: The declare Keyword
┌──────────────────────────────────────────────────────────┐
│ declare const VERSION: string; │
│ "The runtime provides VERSION: string." │
│ No emitted code. The compiler trusts. │
│ │
│ declare function greet(name: string): string; │
│ "The runtime provides greet." │
│ No emitted code. The compiler trusts. │
│ │
│ declare class Logger { log(m: string): void; } │
│ "The runtime provides Logger." │
│ No emitted code. The compiler trusts. │
│ │
│ interface User { id: string; } │
│ No runtime. No declare needed. │
│ │
│ The declare keyword is a claim about the runtime. │
│ │
└──────────────────────────────────────────────────────────┘
Visual: Module vs Script
┌──────────────────────────────────────────────────────────┐
│ MODULE (has a top-level import or export) │
│ │
│ // file.d.ts │
│ export interface User { id: string; } │
│ │
│ The declarations are scoped to the module. │
│ Global augmentation requires `declare global`. │
│ │
├──────────────────────────────────────────────────────────┤
│ SCRIPT (no top-level import or export) │
│ │
│ // globals.d.ts │
│ declare const VERSION: string; │
│ interface Window { myApp: App; } │
│ │
│ The declarations are in the global scope. │
│ No `declare global` needed. │
│ │
└──────────────────────────────────────────────────────────┘
Visual: The declare global Block
┌──────────────────────────────────────────────────────────┐
│ // global.d.ts │
│ export {}; ← makes the file a module │
│ │ │
│ ▼ │
│ declare global { │
│ interface Window { │
│ myApp: { version: string }; │
│ } │
│ │
│ var myGlobal: string; │
│ ▲ │
│ └── var is required (not let/const) │
│ │
│ namespace NodeJS { │
│ interface ProcessEnv { │
│ DATABASE_URL: string; │
│ } │
│ } │
│ } │
│ │
│ The block adds to the global scope. │
│ │
└──────────────────────────────────────────────────────────┘
Visual: The declare module Forms
┌──────────────────────────────────────────────────────────┐
│ NEW MODULE │
│ │
│ declare module 'untyped-lib' { │
│ export function request(url: string): Promise<Response>;│
│ } │
│ │
│ No import. The block declares the module's shape. │
│ The consumer's import is checked against it. │
│ │
├──────────────────────────────────────────────────────────┤
│ AUGMENTATION │
│ │
│ import 'express'; ← makes the file a module │
│ declare module 'express' { │
│ interface Request { user?: User; } │
│ } │
│ │
│ The block augments the existing module. │
│ The interface merge applies. │
│ │
│ Without the import, the block would override the module.│
│ │
└──────────────────────────────────────────────────────────┘
Visual: The Ambient Contexts
┌──────────────────────────────────────────────────────────┐
│ .d.ts FILE │
│ Every declaration is ambient. │
│ declare required for runtime values. │
│ │
│ declare global BLOCK │
│ In a module. Adds to the global scope. │
│ var for globals. │
│ │
│ declare module BLOCK │
│ Declares or augments a module. │
│ Import required for augmentation. │
│ │
│ AMBIENT NAMESPACE │
│ Groups declarations. │
│ The classic global library form. │
│ │
└──────────────────────────────────────────────────────────┘
Visual: The Claim
┌──────────────────────────────────────────────────────────┐
│ declare const VERSION: string; │
│ │ │
│ └── "The runtime provides VERSION as a string." │
│ │
│ The compiler trusts the claim. │
│ It does not verify the runtime. │
│ │
│ If the runtime does NOT provide VERSION: │
│ │ │
│ ▼ │
│ ReferenceError: VERSION is not defined │
│ │
│ The claim is a promise. A wrong promise is a │
│ runtime failure the compiler did not catch. │
│ │
└──────────────────────────────────────────────────────────┘
Summary
| Item | Value |
|---|---|
declare | Ambient declaration |
.d.ts file | Ambient context |
declare global | Global scope, from a module |
declare module 'name' | Declare or augment a module |
| Ambient namespace | Global, grouped |
| Module | Has a top-level export |
| Script | No top-level export |
export {} | Makes a file a module |
var in declare global | Required for globals |
| Wildcard module | declare module '*.ext' |
Key takeaways:
- An ambient declaration is a claim about the runtime — the compiler trusts it, and a wrong claim is a runtime failure the compiler did not catch
- The
declarekeyword introduces the claim — it is required for variables, functions, classes, enums, and namespaces, and it is not used for interfaces and type aliases - A
.d.tsfile is an ambient context — every declaration in it is ambient, and the ones with a runtime representation needdeclare - A module has a top-level import or export; a script does not — the distinction determines whether the declarations are scoped to the file or in the global scope
- The
declare globalblock adds to the global scope from a module — it requires theexport {}, and it requiresvarfor global variables - The
declare module 'name'block declares or augments a module — the import is required for an augmentation, and its absence turns the block into an override - The ambient namespace is the classic global library form — it groups declarations under a name, and the consumer uses the dot notation
- The wildcard module declares a pattern for assets —
declare module '*.css'gives every CSS import a type, and the pattern is used for the imports a bundler handles - The environment variable pattern uses the
NodeJS.ProcessEnvaugmentation — the interface is extended with the specific variables, and the code that reads them is type-safe - The claim should be as precise as the runtime allows — a vague wildcard or an over-broad declaration is a lie that hides errors, and the declaration should describe what the runtime actually provides
Remember: An ambient declaration is the bridge between the untyped runtime and the typed code. The declare keyword introduces the claim, the .d.ts file and the declare global and declare module blocks are the contexts, and the module-vs-script rule determines the scope. Use the declarations for the names the runtime provides, keep the claims precise, and document the source. The ambient declarations are the mechanism that makes the ecosystem’s types work, and understanding them is what makes a .d.ts file readable.
Stop using slow, ad-bloated tool sites! 🤮
🔎 Search “KandZ Tools” on Google to use many professional utilities for free.
KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)
⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free
🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!