Chapter 8.3☕ 16 min read

Ambient Types & .d.ts Files

Photo frames without the person, shapes without the body

01The Photo Frame

Sometimes you have JavaScript code that TypeScript simply does not know about. It could be global variables injected from a script tag in your HTML, values loaded from a legacy JS library, or runtime configurations injected by the server. TypeScript looks at these and sees a ghost — it knows something is there at runtime, but it cannot see its shape!

Ambient types, which live in .d.ts files, let you DESCRIBE the shape of this external code without providing any implementation. Think of them as PHOTO FRAMES without the person inside.

Imagine the portrait gallery in the Nizam's palace. The walls are lined with ornate golden frames. Each frame has a NAME PLATE — "The Grand Vizier", "The Army Commander" — and a DESCRIPTION — "Wears a turban, holds a sword, stands 6 feet tall." But the frames are EMPTY. The actual person is not in the frame, only their description is.

When a visitor (TypeScript) walks through the gallery, they can read the name plates and descriptions and KNOW who should be there, what they look like, and how to address them. The visitor does not need the actual person to be present — the DESCRIPTION is enough to understand the protocol.

// This is a photo frame - no body inside!
declare const API_URL: string;

// TypeScript now knows API_URL exists
console.log(API_URL.toLowerCase());

.d.ts files are those ornate frames. They describe the SHAPE of code that exists elsewhere (in JavaScript), without containing the actual code. TypeScript reads the descriptions and understands the types, even though the implementation is in a completely different language or context.

Seedha samjho: .d.ts = photo frame, shape batata hai, body nahi!

02Declaration File Basics

Declaration files use the .d.ts extension and contain ONLY type declarations — no runtime code, no values, no implementations whatsoever. If you try to write actual code in a .d.ts file, TypeScript will immediately throw an error. The declare keyword is your primary tool here.

Declaring a variable:

// Tells TS: "Trust me, this exists at runtime"
declare const API_URL: string;

TypeScript now knows API_URL is a global string. You do not need to import it — it is just there, like the air you breathe. This is perfect for variables injected by Webpack, Vite, or server-side templates directly into your HTML.

Declaring a function:

declare function fetchUser(
  id: string
): Promise<User>;

TypeScript knows about fetchUser and will check your arguments, but there is zero JavaScript code emitted for this in the .d.ts file. The actual function lives elsewhere — maybe in an old script tag or a legacy bundle.

Declaring a class:

declare class NotificationService {
  send(msg: string): void;
}

You can instantiate and use this class with full type safety, even though the class implementation is in plain JavaScript that TypeScript has never seen.

Declaring a module:

declare module "legacy-lib" {
  export function doSomething(
    x: number
  ): string;
  export const version: string;
}

Now you can import from "legacy-lib" without TypeScript complaining that it cannot find the module. File naming conventions: types.d.ts, global.d.ts, express.d.ts. These files are AUTOMATICALLY included by TypeScript — you do not import them (unless they are module declarations). They sit quietly in your project and TypeScript picks them up on its own.

03Creating .d.ts Files

When do you actually write your own declaration files? Let us walk through the four most common real-world scenarios where .d.ts files are essential.

1. Global variables from HTML

// global.d.ts
declare const APP_CONFIG: {
  apiURL: string;
  env: string;
};

Your server injects a APP_CONFIG object into the HTML template. TypeScript has no idea this exists unless you declare it. Now you get autocomplete and type checking for APP_CONFIG.apiURL without importing anything.

2. Legacy JavaScript code

// legacy.d.ts
declare module "old-calc-lib" {
  export function add(
    a: number, b: number
  ): number;
  export function subtract(
    a: number, b: number
  ): number;
}

That ancient calculator library from 2015 has no types. Instead of rewriting it, you create a declaration file that describes its API. TypeScript is happy, you are happy, and the legacy code stays untouched.

3. Extending existing types (Module Augmentation)

// express.d.ts
declare module "express" {
  interface Request {
    userId?: string;
  }
}

Express's Request object does not have a userId by default. With module augmentation, you can add custom properties to existing third-party types. This is incredibly powerful for middleware that attaches data to requests.

4. Global types for testing

// test-globals.d.ts
declare namespace NodeJS {
  interface Global {
    __TEST__: boolean;
  }
}

The triple-slash directive /// <reference types="node" /> explicitly references declaration files. You can put your declarations in a types/ folder and they will be auto-included. Customize where TypeScript looks using the typeRoots option in tsconfig.json. Keep your frames organized!

04Declaration File Traps

Declaration files have sharp edges that trip up even experienced developers. Let us walk through the most common traps one by one.

Trap 1: Putting runtime code in .d.ts files

// ERROR! .d.ts cannot have implementations
declare function fn(): string;
function fn() {
  return "hello";
}

.d.ts files CANNOT contain implementations. Split them: types go in .d.ts, implementation goes in .ts. This is the most fundamental rule of ambient types — you are describing a shadow, not casting the body.

Trap 2: Declaring but not providing

// This tells TS that MAGIC exists...
declare const MAGIC: number;

// But if it doesn't exist at runtime...
console.log(MAGIC.toFixed(2));
// RUNTIME ERROR! MAGIC is undefined

Declarations are PROMISES, not guarantees. You told TypeScript that MAGIC exists, so it compiles fine. But if nobody actually provides MAGIC at runtime, your code crashes. Always ensure the runtime environment actually delivers what you declared.

Trap 3: Module augmentation pitfalls

// This might NOT work if express
// is never imported in this file!
declare module "express" {
  interface Request {
    userId?: string;
  }
}

When using declare module "express", you must IMPORT express somewhere in the file for the augmentation to activate. Just having the declare block in a random .d.ts might not be enough — TypeScript needs to know the module is actually in scope.

Trap 4: Duplicate declarations — two .d.ts files declaring the same global variable with different types causes a conflict. Keep declarations in one single place.

Trap 5: Forgetting to include the .d.ts — if your types folder is not in include or typeRoots in tsconfig.json, TypeScript might completely ignore your declarations.

Trap 6: Using any in declarationsdeclare const data: any; defeats the entire purpose of typing! Always provide specific, precise types in your declarations.

05Ambient Types Cheatsheet

Let us lock in everything about ambient types and .d.ts files with a quick cheatsheet you can refer back to anytime.

Core Concept

// Describe types without implementation
// Photo frames without the person inside

File Extension

// .d.ts — automatically included by TS
// global.d.ts, types.d.ts, lib.d.ts

The declare Keyword

declare var myGlobal: string;
declare const API_URL: string;
declare function fetch(id: string): void;
declare class MyService { run(): void; }
declare module "my-lib" { ... }

Module Augmentation

declare module "express" {
  interface Request {
    userId?: string;
  }
}

Triple-Slash Directive

/// <reference types="node" />
// Explicitly references declaration files

4 Main Use Cases

  • Global variables injected by HTML/server templates.
  • Legacy JavaScript libraries with no type definitions.
  • Extending third-party types with custom properties.
  • Runtime-injected values like test globals and feature flags.

6 Key Rules

  • No runtime code in .d.ts files — only declarations.
  • Declarations are promises — ensure runtime provides the values.
  • Use specific types, never any.
  • Keep declarations organized in a types/ folder.
  • Module augmentation needs an import to activate.
  • Check tsconfig.json includes your declaration files.

The golden rule: ".d.ts files are the Nizam's portrait frames — they describe who should be there without the actual person. Describe the shape precisely, bhai, but make sure the real body exists at runtime!"

Key Points

  • .d.ts files describe the shape of code without implementations
  • The declare keyword tells TypeScript that a construct exists elsewhere
  • .d.ts files are automatically included by the TypeScript compiler
  • Declarations are promises — ensure the runtime actually provides the values
  • Use module augmentation to extend third-party types
  • Never put runtime code or use the any type in .d.ts files
Course Search
Search across all chapters & stages
📖

Search the course

Type any topic — branching, stash, rebase, hooks — and jump straight to that chapter.

merge branchesgit stashundo commitrebase