Ambient Modules & Globals
Ambient in TypeScript means "declared but not implemented here." Ambient modules and globals are TypeScript's way of integrating with JavaScript that TypeScript cannot directly inspect — browser APIs, globally loaded scripts, CDN libraries, environment variables, and non-JavaScript assets.
Understanding ambient declarations unlocks the ability to use any JavaScript in TypeScript with full type safety — even code that predates TypeScript entirely.
What Makes a Declaration Ambient?
Any declaration that uses declare (or lives inside a .d.ts file) is ambient.
It tells the compiler: "This exists at runtime — accept it, type-check it, but
don't emit JavaScript for it."
There are three places ambient declarations live:
- Inside
.d.tsfiles — dedicated declaration files - Inside
.tsfiles usingdeclare— inline ambient declarations - Provided by
@types/*packages — community-maintained declarations
Ambient Module Declarations
An ambient module declaration tells TypeScript what types a module exports when TypeScript cannot find the module's source. This is the bridge between untyped JavaScript packages and your TypeScript code.
// vendor-types.d.ts
// Provide types for an untyped npm package
declare module 'some-legacy-library' {
export interface Options {
timeout: number;
retries: number;
baseUrl: string;
}
export class Client {
constructor(options: Options);
get(path: string): Promise<unknown>;
post(path: string, data: unknown): Promise<unknown>;
}
export function createClient(options: Options): Client;
const defaultExport: {
Client: typeof Client;
createClient: typeof createClient;
VERSION: string;
};
export default defaultExport;
}
Wildcard Module Declarations
Bundlers let you import non-JavaScript files — CSS, SVG, images, text, WASM.
TypeScript does not know how to resolve these natively. Wildcard module declarations
use a glob-like * to match whole categories of imports:
// assets.d.ts
// CSS Modules
declare module '*.module.css' {
const classes: { readonly [key: string]: string };
export default classes;
}
declare module '*.module.scss' {
const classes: { readonly [key: string]: string };
export default classes;
}
// Images
declare module '*.png' { const src: string; export default src; }
declare module '*.jpg' { const src: string; export default src; }
declare module '*.jpeg' { const src: string; export default src; }
declare module '*.gif' { const src: string; export default src; }
declare module '*.webp' { const src: string; export default src; }
declare module '*.avif' { const src: string; export default src; }
// Vector and fonts
declare module '*.svg' { const src: string; export default src; }
declare module '*.woff' { const src: string; export default src; }
declare module '*.woff2' { const src: string; export default src; }
// Data
declare module '*.txt' { const content: string; export default content; }
declare module '*.md' { const content: string; export default content; }
declare module '*.yaml' { const data: unknown; export default data; }
declare module '*.toml' { const data: unknown; export default data; }
// WebAssembly
declare module '*.wasm' {
const init: (imports?: WebAssembly.Imports) => Promise<WebAssembly.Instance>;
export default init;
}
// Consumer — TypeScript accepts all these imports
import logo from './logo.png'; // string (URL)
import styles from './app.module.css'; // { readonly [key: string]: string }
import template from './email.txt'; // string
function App() {
return `<img src="${logo}" class="${styles.container}">${template}</img>`;
}
Shorthand Ambient Modules
If you just want to silence "Cannot find module" errors for a package without
providing real types yet, use a shorthand ambient module — a single-line declaration
that makes all imports from that module resolve as any:
// quick-types.d.ts
// Shorthand — all exports are 'any'
declare module 'some-untyped-package';
declare module 'another-legacy-lib';
// You can still import these without errors — but lose type safety
import { anything } from 'some-untyped-package'; // anything: any
any. Replace them with proper type declarations as soon as practical.Ambient Global Variables
Scripts loaded with <script> tags inject globals into the window object.
Ambient globals describe these without any import:
// browser-globals.d.ts — no import/export at top level (script context)
// Google Analytics 4
declare function gtag(
command: 'config',
targetId: string,
params?: Record<string, unknown>,
): void;
declare function gtag(
command: 'event',
eventName: string,
params?: Record<string, unknown>,
): void;
declare function gtag(command: 'set', params: Record<string, unknown>): void;
// Google reCAPTCHA v3
declare const grecaptcha: {
ready(callback: () => void | Promise<void>): void;
execute(siteKey: string, options: { action: string }): Promise<string>;
};
// Facebook Pixel
declare const fbq: (
command: 'track' | 'trackCustom' | 'init',
event: string,
params?: Record<string, unknown>,
) => void;
// Intercom
declare function Intercom(command: string, ...args: unknown[]): void;
Extending Built-in Globals
You can extend existing global interfaces like Window, Document,
HTMLElement, and Navigator by re-declaring them with additional members.
TypeScript merges the declarations:
// global-extensions.d.ts (script file — no import/export)
interface Window {
// Custom app state attached to window
__APP_CONFIG__: {
apiUrl: string;
featureFlags: Record<string, boolean>;
version: string;
};
// Service worker registration
workbox?: {
messageSkipWaiting(): void;
};
}
interface Navigator {
// Non-standard but widely supported
getBattery(): Promise<{
charging: boolean;
level: number;
chargingTime: number;
dischargingTime: number;
}>;
// Web Share API
share(data: { title?: string; text?: string; url?: string }): Promise<void>;
canShare(data: { files?: File[] }): boolean;
}
// Extend Array prototype (rarely needed, but supported)
interface Array<T> {
last(): T | undefined;
groupBy<K extends string>(fn: (item: T) => K): Record<K, T[]>;
}
Ambient Globals from a Module File
If your global declarations file imports anything, it becomes a module. To still
add globals from a module file, wrap them in declare global:
// setup-globals.ts — this file has an import, so it is a module
import type { User } from './user'; // this makes the file a module
declare global {
// These go into the global scope despite the file being a module
interface Window {
currentUser: User | null;
logout(): void;
}
var __sessionId: string; // use 'var' for global augmentation (not const/let)
type DeepPartial<T> = {
[K in keyof T]?: T[K] extends object ? DeepPartial<T[K]> : T[K];
};
}
// Regular module code can follow
export function initGlobals(user: User) {
window.currentUser = user;
window.logout = () => { window.currentUser = null; };
}
declare global, use var instead of const/let for variable declarations. Only var is valid in the global augmentation context.Ambient Module Augmentation
You can extend types from any module — including node_modules packages — using
ambient module augmentation. This is the standard pattern for adding custom
properties to framework types like Express Request, React component props,
or Prisma models:
// augment-express.d.ts
import 'express';
declare module 'express-serve-static-core' {
interface Request {
user?: {
id: string;
email: string;
roles: ('admin' | 'editor' | 'viewer')[];
};
startTime: number;
correlationId: string;
}
interface Response {
sendSuccess<T>(data: T, statusCode?: number): void;
sendError(message: string, statusCode?: number): void;
}
}
// augment-vitest.d.ts — add custom matchers
import type { Assertion, AsymmetricMatchersContaining } from 'vitest';
declare module 'vitest' {
interface Assertion<T = any> {
toBeWithinRange(min: number, max: number): void;
toBeValidUUID(): void;
toBeISODateString(): void;
}
interface AsymmetricMatchersContaining {
withinRange(min: number, max: number): unknown;
}
}
Ambient Declarations and process.env
One of the most common ambient augmentations is typing process.env for Node.js
and Next.js projects:
// env.d.ts — type your environment variables
declare namespace NodeJS {
interface ProcessEnv {
// Required variables — TypeScript enforces they exist
readonly NODE_ENV: 'development' | 'production' | 'test';
readonly DATABASE_URL: string;
readonly NEXTAUTH_SECRET: string;
// Public variables (NEXT_PUBLIC_ prefix for Next.js)
readonly NEXT_PUBLIC_API_URL: string;
readonly NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY: string;
// Optional variables
readonly LOG_LEVEL?: 'debug' | 'info' | 'warn' | 'error';
readonly PORT?: string;
readonly SENTRY_DSN?: string;
}
}
// Usage — process.env is now fully typed
const apiUrl = process.env.NEXT_PUBLIC_API_URL; // string ✓
const port = parseInt(process.env.PORT ?? '3000', 10); // string | undefined → number ✓
// TypeScript catches typos in env var names
process.env.NEXT_PUBLIC_API_URLS; // ✗ Error: Property does not exist
lib.d.ts and TypeScript's Built-in Ambient Declarations
TypeScript ships with hundreds of ambient declaration files in its lib/ folder
— these describe the JavaScript standard library, DOM APIs, Web Workers, and more.
The lib compiler option controls which ones are included:
// tsconfig.json
{
"compilerOptions": {
// Include DOM types + modern JS standard library
"lib": ["ES2022", "DOM", "DOM.Iterable"],
// For Node.js — don't include DOM
// "lib": ["ES2022"]
// For Web Workers
// "lib": ["WebWorker", "ES2022"]
}
}
lib value | What it includes |
|---|---|
ES5 | Array, Math, JSON, Date basics |
ES2020 | BigInt, globalThis, optional chaining types |
ES2022 | Array.at, Object.hasOwn, class fields |
ESNext | Latest TC39 proposals |
DOM | Window, Document, HTMLElement, fetch, etc. |
DOM.Iterable | NodeList[Symbol.iterator], etc. |
WebWorker | WorkerGlobalScope, postMessage, etc. |
Triple-Slash Reference Directives
Triple-slash directives are special XML comments at the top of a file that instruct the compiler to include additional declaration files:
/// <reference types="node" /> // include @types/node
/// <reference types="jest" /> // include @types/jest
/// <reference lib="dom" /> // include built-in DOM lib
/// <reference path="./custom-types.d.ts" /> // include a specific .d.ts
// Use these when:
// 1. You need a @types package in a file but don't want it globally
// 2. You are authoring a .d.ts file that depends on another
// 3. You need to reference a lib that isn't in your tsconfig "lib" array
Common Ambient Module Patterns Summary
Pattern | Syntax | When to use |
|---|---|---|
Specific module types | declare module 'lodash' {...} | Typing one untyped package |
Wildcard module types | declare module '*.svg' {...} | Typing all files of a type |
Shorthand (escape hatch) | declare module 'x' | Silence errors quickly (loses types) |
Global variables | declare const x: T | Globals from script tags |
Extend globals | interface Window { x: T } | Add to existing global types |
Global from module | declare global { var x: T } | Globals declared in a module file |
Augment a package | declare module 'express' {...} | Add members to package types |
Type process.env | namespace NodeJS { interface ProcessEnv {...} } | Typed env variables |
Summary
Ambient declarations describe JavaScript that exists at runtime but is invisible to TypeScript.
Wildcard module declarations (declare module "*.png") type whole categories of asset imports.
Shorthand ambient modules (declare module "x") silence errors quickly but lose type safety.
Global declaration files (no import/export) add types to the global TypeScript scope.
Use declare global inside module files to add to the global scope without making a script.
Module augmentation (declare module "express") extends types from existing packages.
Type process.env via NodeJS.ProcessEnv augmentation for safe environment variable access.
TypeScript's lib option controls which built-in ambient declarations are included.