Lesson 30 · Module 5.1 — Tooling & ecosystem

Reading declare and .d.ts: types with no runtime

You open a library and find a file full of types but no function bodies — every line starts with declare or just describes shapes. Coming from OO, it reads like a header file or an interface-only module. That's exactly what it is. This lesson decodes the one keyword and the one file extension that mean "the code exists at runtime; here is only its type."

The one rule

declare means "ambient": this thing already exists at runtime — here is its type, emit no JavaScript for it." A .d.ts file is a file made entirely of such declarations: pure type information, zero runtime code. A .ts file holds real code that compiles to .js; a .d.ts file compiles to nothing.

What declare says to the compiler

A normal declaration creates something. An ambient declaration only promises it is already there — supplied by the runtime, by another script, or by a JS library with no types of its own. The compiler trusts the promise, type-checks against it, and emits nothing:

// "There is a global called VERSION, of type string. Don't emit it; trust me."
declare const VERSION: string;

// "There is a function gtag(...). I won't give a body — the runtime provides it."
declare function gtag(command: string, ...args: unknown[]): void;

Notice there is no = and no { ... } body. That absence is the tell: an ambient declaration is all type, no value. Try to give it a body and the compiler objects — that would be real code, which is the one thing declare forbids.

.ts vs .d.ts: the file is the boundary

The extension tells you which world the whole file lives in:

filecontainscompiles toyou write it when…
.tsreal code and typesa .js fileyou are writing the actual program
.d.tsdeclarations only — no runtime codenothing (it is the types)you describe code that already runs

Inside a .d.ts file the declare keyword is often implied — the file is ambient by nature — so you'll see bare interface, type, and function signatures. They look like the body-less methods of an OO interface, and they play the same role: a contract with no implementation.

// math-utils.d.ts — describes a plain-JS library that ships no types
export function clamp(n: number, lo: number, hi: number): number;
export interface Range { min: number; max: number; }

Where these files come from

You rarely hand-write .d.ts files. Two common origins:

So when you read one, picture it as the type-only shadow of some JavaScript that exists elsewhere. The implementation is real and runs; the .d.ts is the part TypeScript reads.

Script vs module: what decides the scope

Before the trap below makes sense, you need the rule that decides a file's scope. TypeScript sorts every file into one of two kinds using a single test — does the file have a top-level import or export?

top-level import/export?the file is a…its top-level names live in…
nonescriptthe global scope — one space shared by the whole project
at least onemodulethat file's own scope — private unless you export it

So at the top level there are really only two scopes to keep in mind: the single global scope that every script and every ambient declaration shares, and a separate module scope for each file that is a module. A name in the global scope is visible everywhere with no import — and can collide with any other global of the same name. A name in a module scope is invisible until someone imports it.

A .d.ts is ambient by nature, so unless it contains an import or export it counts as a script — and everything in it lands in that one shared global scope. Adding a single top-level line flips the whole file to a module:

// globals.d.ts — no import/export, so this file is a SCRIPT
interface User { id: string; }   // global: visible everywhere, no import

// ...add this one top-level line and the file becomes a MODULE —
// now the User above is file-local, not global:
export {};

The trap: don't write your own types in a .d.ts

It is tempting to use a .d.ts as a handy place to keep your project's types — they become available everywhere without importing anything. But that is the wrong tool. A .d.ts is meant to describe code that already runs somewhere else (a JS library, a browser global). It is not where the types for the code you are writing belong. Two reasons this matters:

Common mistake

Dropping interface User { … } into a hand-written .d.ts "so I can use it everywhere" does work — and that is exactly the trap. You have made a global with no import trail. Your own types belong in a normal .ts file that you export.

// AVOID — globals.d.ts has no import/export, so this leaks into EVERY file
interface User { id: string; name: string; }   // now global; nothing imports it

// PREFER — models/user.ts is a real module: explicit, scoped, refactor-friendly
export interface User { id: string; name: string; }
// elsewhere:  import type { User } from "./models/user";

So when is an interface or type inside a .d.ts fine? Only when it is describing something that already exists at runtime — an untyped JS library (like the math-utils.d.ts above, which describes someone else's code), a global the page sets up, or a non-code import:

// Legit .d.ts work — every line describes something that already exists at runtime
declare global { interface Window { dataLayer: unknown[]; } }  // a global the page injects
declare module "*.svg" { const src: string; export default src; }  // non-code import

Rule of thumb: if the type is for code you are writing, put it in a .ts file and export it. Only reach for a .d.ts to describe code that already exists but does not come with its own types.

Check yourself

Recall, don’t re-read. (Answers reveal on click.)

What does declare const VERSION: string; emit into the output JavaScript?
A .d.ts file differs from a .ts file in that it holds…
When would you author a hand-written .d.ts file yourself?
You write interface User { … } in a globals.d.ts that has no import or export. What happens?
What turns a TypeScript file from a global script into a module (so its top-level names become file-local)?

Primary source

The official handbook entry points are TypeScript Handbook — Declaration Files (why and how .d.ts exists) and Modules — Declaration Files (how a .d.ts describes a module). For the keyword itself, see Declaration Files — By Example. Best way to feel it: in the TS Playground, set declaration: true and watch the .d.ts panel mirror your code with every body removed.

Try it yourself

Staring at a body-less file and unsure if it's runtime code or just types? Paste a few lines and ask “.ts or .d.ts — does this emit JavaScript?” Next lesson (5.2) moves from describing modules to finding them: module vs moduleResolution.