TypeScriptAmbient Modules & Globals

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:

  1. Inside .d.ts files — dedicated declaration files
  2. Inside .ts files using declare — inline ambient declarations
  3. 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.

TS
// 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:

TS
// 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;
}
      

TS
// 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:

TS
// 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
      
Warning
Shorthand ambient modules are a shortcut, not a solution. They silence errors by making everything 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:

TS
// 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:

TS
// 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:

TS
// 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; };
}
      
Note
Inside 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:

TS
// 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;
  }
}
      

TS
// 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:

TS
// 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;
  }
}
      

TS
// 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:

JSON
// 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:

TS
/// <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.