Skip to content

Declaration Files and Ambient Types

advanced12 min read

Types Without Implementation

Ever wonder what's inside those @types/* packages you install from npm? Declaration files. .d.ts files describe the shape of JavaScript code without containing any implementation. They're the bridge between TypeScript's type system and the vast untyped JavaScript ecosystem.

Mental Model

Think of declaration files as blueprints without the building. A .d.ts file describes what a module exports, what functions accept and return, and what global variables exist — but contains zero executable code. It's like a restaurant menu — it tells you what's available and what each item contains, without being the food itself.

The declare Keyword

declare tells TypeScript "this thing exists, trust me, but I'm not providing the implementation here":

// Declare a global variable (exists at runtime but not in TS)
declare const API_URL: string;
declare const __DEV__: boolean;

// Declare a function
declare function analytics(event: string, data?: Record<string, unknown>): void;

// Declare a class
declare class Logger {
  constructor(prefix: string);
  log(message: string): void;
  error(message: string, error?: Error): void;
}

// Declare a module
declare module "legacy-lib" {
  export function doThing(x: number): string;
  export const VERSION: string;
}
Common Trap

declare produces NO JavaScript output. It's purely a type-level construct. If you declare a variable that doesn't actually exist at runtime, TypeScript won't warn you — but your code will crash:

declare const magicValue: number; // TypeScript trusts this exists
console.log(magicValue); // No TS error — but ReferenceError at runtime if it doesn't exist

// Only use declare for things that genuinely exist at runtime
// (globals injected by the build tool, external scripts, etc.)

Anatomy of a .d.ts File

// types/analytics.d.ts

// Module declaration — describes what "analytics-lib" exports
declare module "analytics-lib" {
  interface Event {
    name: string;
    properties?: Record<string, string | number | boolean>;
    timestamp?: number;
  }

  interface AnalyticsConfig {
    apiKey: string;
    endpoint?: string;
    debug?: boolean;
  }

  export function init(config: AnalyticsConfig): void;
  export function track(event: Event): void;
  export function identify(userId: string, traits?: Record<string, unknown>): void;
  export function page(name: string, properties?: Record<string, unknown>): void;

  // Default export
  const analytics: {
    init: typeof init;
    track: typeof track;
    identify: typeof identify;
    page: typeof page;
  };
  export default analytics;
}

Global Declarations

For types that should be available everywhere without importing:

// types/global.d.ts

// Global variable declarations
declare const __APP_VERSION__: string;
declare const __BUILD_TIME__: string;

// Extend the Window interface
declare global {
  interface Window {
    dataLayer: Record<string, unknown>[];
    gtag: (...args: unknown[]) => void;
  }

  // Global function
  function requestIdleCallback(
    callback: (deadline: IdleDeadline) => void,
    options?: { timeout: number }
  ): number;

  // Global type (available without import)
  type Nullable<T> = T | null | undefined;
}

// This empty export makes the file a module (required for declare global)
export {};
Script mode vs module mode in .d.ts files

TypeScript treats files differently based on whether they have imports/exports:

  • Script mode (no imports/exports): All declarations are global automatically. No declare global needed.
  • Module mode (has import or export): Declarations are scoped to the module. Use declare global {} to add global types.
// Script mode — these are automatically global
declare const VERSION: string;
interface AppConfig { /* ... */ }

// Module mode — must use declare global
import type { Something } from "somewhere";

declare global {
  const VERSION: string;
  interface AppConfig { /* ... */ }
}

export {}; // Makes this a module

Adding export {} at the end is a common pattern to force module mode when you need both imports and global declarations.

Writing Types for Untyped Libraries

Sooner or later, you'll hit a library that doesn't have types and no @types/* package exists. Here's how you write your own:

// types/untyped-charts.d.ts
declare module "untyped-charts" {
  interface ChartOptions {
    type: "line" | "bar" | "pie" | "scatter";
    data: {
      labels: string[];
      datasets: {
        label: string;
        data: number[];
        color?: string;
      }[];
    };
    responsive?: boolean;
    animation?: boolean | { duration: number; easing: string };
  }

  export class Chart {
    constructor(element: HTMLCanvasElement, options: ChartOptions);
    update(data: ChartOptions["data"]): void;
    destroy(): void;
    resize(): void;
  }

  export function createChart(
    selector: string,
    options: ChartOptions
  ): Chart;
}

Wildcard Module Declarations

You've probably seen these before — they handle non-JavaScript imports that your bundler processes:

// types/assets.d.ts

// CSS modules
declare module "*.module.css" {
  const classes: Record<string, string>;
  export default classes;
}

// Image imports
declare module "*.png" {
  const src: string;
  export default src;
}

declare module "*.svg" {
  import type { FC, SVGProps } from "react";
  const SVGComponent: FC<SVGProps<SVGSVGElement>>;
  export default SVGComponent;
}

// JSON imports (if not using resolveJsonModule)
declare module "*.json" {
  const value: unknown;
  export default value;
}

Production Scenario: Typing a Legacy JavaScript SDK

// types/payment-sdk.d.ts
// Typing a third-party payment SDK loaded via script tag

declare namespace PaymentSDK {
  interface CardElement {
    mount(selector: string): void;
    unmount(): void;
    on(event: "change", handler: (state: CardState) => void): void;
    on(event: "error", handler: (error: PaymentError) => void): void;
    on(event: "ready", handler: () => void): void;
  }

  interface CardState {
    complete: boolean;
    brand: "visa" | "mastercard" | "amex" | "unknown";
    last4?: string;
  }

  interface PaymentError {
    code: string;
    message: string;
    type: "validation_error" | "api_error" | "network_error";
  }

  interface PaymentResult {
    id: string;
    status: "succeeded" | "requires_action" | "failed";
    amount: number;
    currency: string;
  }

  interface SDK {
    createElement(type: "card", options?: { style?: Record<string, string> }): CardElement;
    confirmPayment(options: {
      clientSecret: string;
      element: CardElement;
    }): Promise<PaymentResult>;
  }
}

declare global {
  interface Window {
    PaymentSDK: {
      init(publishableKey: string): PaymentSDK.SDK;
    };
  }
}

export {};
Execution Trace
Script
`<script src='payment-sdk.js'>`
SDK loaded via script tag — no module import
Declare
declare namespace PaymentSDK
Describes the SDK's types without implementation
Global
declare global { interface Window { PaymentSDK } }
Tells TypeScript window.PaymentSDK exists
Use
const sdk = window.PaymentSDK.init('pk_123')
Fully typed — autocomplete for all methods and properties
What developers doWhat they should do
Putting implementation code in .d.ts files
Declaration files describe shapes. The implementation exists elsewhere (JavaScript files, runtime globals).
.d.ts files contain only type declarations — no runtime code, no function bodies, no class implementations
Forgetting export {} when using declare global in a file with imports
In script mode, all declarations are already global. declare global is specifically for module-mode files.
Add export {} or any export to make the file a module — declare global only works in module-mode files
Using declare for values that don't actually exist at runtime
declare tells TypeScript 'trust me, this exists.' If it doesn't, you get ReferenceError at runtime.
Only declare things that genuinely exist — injected globals, script-loaded libraries, build-time constants
Writing overly permissive wildcard module declarations (declare module '*')
Overly broad wildcards silence legitimate type errors. Specific declarations catch real bugs.
Be specific: declare module '*.css', declare module '*.png' — each with accurate types
Quiz
What's the difference between a .ts file and a .d.ts file?
Quiz
When do you need 'declare global' vs just writing declarations at the top level?
Quiz
What does declare module '*.css' do?

Challenge: Write Types for a Legacy Library

// Write a complete .d.ts file for this untyped JavaScript library:
//
// Usage (from the library's README):
//
// import { createStore, combineReducers } from 'mini-store';
//
// const store = createStore(reducer, initialState);
// store.getState(); // returns the current state
// store.dispatch({ type: 'INCREMENT' }); // dispatches an action
// store.subscribe(listener); // returns an unsubscribe function
//
// const rootReducer = combineReducers({
//   counter: counterReducer,
//   todos: todosReducer,
// });
//
// Reducer signature: (state, action) => newState
// Action must have a 'type' string property

// Your .d.ts file here:
// types/mini-store.d.ts
declare module "mini-store" {
  interface Action {
    type: string;
    [key: string]: unknown;
  }

  type Reducer<S> = (state: S, action: Action) => S;
  type Listener = () => void;
  type Unsubscribe = () => void;

  interface Store<S> {
    getState(): S;
    dispatch(action: Action): void;
    subscribe(listener: Listener): Unsubscribe;
  }

  export function createStore<S>(
    reducer: Reducer<S>,
    initialState: S
  ): Store<S>;

  export function combineReducers<M extends Record<string, Reducer<any>>>(
    reducers: M
  ): Reducer<{
    [K in keyof M]: M[K] extends Reducer<infer S> ? S : never;
  }>;
}

The combineReducers type uses infer to extract each reducer's state type and builds the combined state shape automatically. createStore is generic over the state type S, which flows through to getState() and the reducer parameter.

Key Rules
  1. 1.d.ts files contain only type declarations — zero runtime code, zero JavaScript output
  2. 2declare tells TypeScript something exists without providing implementation — use only for genuinely existing values
  3. 3Script-mode files (no imports/exports) make declarations global automatically. Module-mode requires declare global {}.
  4. 4Use specific wildcard modules (*.css, *.png) rather than broad wildcards (*) for asset imports
  5. 5Check DefinitelyTyped (@types/*) before writing custom declarations — most popular libraries have community types