Menu

TypeScript Declare: .d.ts Declaration Files Explained

A .d.ts file describes the types of JavaScript code without containing any of it, and the declare keyword does the same inside a .ts file. Learn how declaration files are generated, where @types packages fit, how to type an untyped module, and how declare global and module augmentation extend existing types.

This page includes runnable editors - edit, run, and see output instantly.

A declaration file (.d.ts) contains types and nothing else: signatures, interfaces and class shapes for JavaScript that lives somewhere else. The declare keyword does the same job inside an ordinary file. It tells the compiler "this exists at run time, trust me", and emits no code.

The compiler accepted __APP_VERSION__.length because the declaration says it is a string. At run time no such variable exists, so reading .length throws ReferenceError: __APP_VERSION__ is not defined, a run-time exception the compiler never saw coming. That is the contract of every declaration: the types are only as true as the JavaScript behind them.

What Goes in a .d.ts File

Compiling with declaration: true shows the idea best. From this source:

// price.ts
export interface LineItem {
    name: string;
    price: number;
    qty: number;
}

const TAX = 0.2;

export function total(items: LineItem[]) {
    const sum = items.reduce((acc, item) => acc + item.price * item.qty, 0);
    return Math.round(sum * (1 + TAX) * 100) / 100;
}

export class Cart {
    private items: LineItem[] = [];
    add(item: LineItem) {
        this.items.push(item);
        return this;
    }
}

tsc writes price.js and this price.d.ts:

export interface LineItem {
    name: string;
    price: number;
    qty: number;
}
export declare function total(items: LineItem[]): number;
export declare class Cart {
    private items;
    add(item: LineItem): this;
}

Function bodies are gone, inferred return types are written out (number, this), the private field keeps its name but loses its type, and the unexported TAX is not there at all. A library publishes the .js for Node and the .d.ts for your editor and compiler. emitDeclarationOnly: true produces only the .d.ts files, for projects where a bundler builds the JavaScript.

The ES2022 built-ins you use every day come from declaration files too: lib.es2022.d.ts and friends ship with TypeScript and are selected by target and lib.

The declare Forms

Every declare statement describes something that already exists. In a .d.ts file with no import or export (a global declaration file), each of these becomes visible to the whole project:

// globals.d.ts
declare const API_URL: string;                     // a global constant
declare let debugMode: boolean;                    // a global variable
declare function track(event: string, props?: Record<string, string>): void;
declare class Widget {                             // a class from a script tag
    constructor(el: string);
    render(): void;
}
declare namespace Analytics {                      // a global object
    function page(name: string): void;
}
declare module "legacy-charts" {                   // a module you import
    export function draw(data: number[]): void;
}

Inside a .d.ts file, interface and type need no declare; any other top-level declaration without declare or export is error TS1046. Once a file has an import or export, its declarations are local to it, and additions to the global scope go in a declare global block (shown below). The declared signature is what the compiler checks calls against:

The compiler reports index.ts(5,21): error TS2322: Type 'number' is not assignable to type 'string'. Pass { items: "3" }, or change the declaration if the real function accepts numbers.

@types Packages

Many npm packages ship their own .d.ts files, referenced from the types field (or a types condition in exports) of their package.json. For JavaScript-only packages, the community-maintained DefinitelyTyped project publishes types under the @types scope:

npm i lodash
npm i -D @types/lodash

When you import a package, TypeScript looks for node_modules/@types/{name} automatically. Types that describe globals rather than an import, such as @types/node (process, Buffer) or a test runner's describe and it, must be listed in tsconfig.json:

{
    "compilerOptions": {
        "types": ["node"]
    }
}

Without that entry, TypeScript 7 does not load them, even when the package is installed:

error TS2591: Cannot find name 'process'. Do you need to install type definitions for node? Try `npm i --save-dev @types/node` and then add 'node' to the types field in your tsconfig.

tsc --init writes "types": [] into the new config, with a comment suggesting ["node"] for Node projects.

Typing an Untyped Module

Importing a JavaScript package that has no types, bundled or in @types, is error TS7016:

error TS7016: Could not find a declaration file for module 'fakelib'. '/project/node_modules/fakelib/index.js' implicitly has an 'any' type.

Fix it with a .d.ts file anywhere in your project (a types/ folder is common; it only has to be covered by include) that has no top-level import or export. Describe the parts you use:

// types/fakelib.d.ts
declare module "fakelib" {
    export function hi(name: string): string;
    export const version: string;
}

// Non-code files a bundler lets you import
declare module "*.svg" {
    const url: string;
    export default url;
}

The shortest version, declare module "fakelib"; on one line, makes every import from the package any. It removes the error and every check with it, so treat it as a temporary step.

declare global

Code that adds to the global scope, such as a polyfill, a new method on a built-in, or a global set by a script tag, needs the matching types. declare global adds them. It must sit in a module (a file with an import or export), which is why export {} is at the top:

interface Array<T> merges with the built-in Array interface instead of replacing it, and var (not let or const) is what adds a property to globalThis. Extending built-in prototypes is risky in shared code; the same declare global technique is how projects add their own variables to process.env in @types/node's NodeJS.ProcessEnv interface.

Module Augmentation

To add to the types of a package you import, redeclare its module name and reopen the interface. The file must itself be a module (the import does that; export {} works too). Without an import or export, the same block declares a brand new config-lib module that hides the package's real types:

// types/config-lib.d.ts
import "config-lib";

declare module "config-lib" {
    interface Settings {
        beta: boolean; // merged into the package's own Settings interface
    }
}

After this, load().beta from config-lib is typed boolean everywhere. Interfaces merge; type aliases do not, so a library has to export an interface for this to work. This is how plugins add fields to a framework's request or config objects.

skipLibCheck

skipLibCheck: true stops the compiler from type checking .d.ts files, including those in node_modules. Your own code is still checked against them. It saves time and avoids errors from two packages whose declarations disagree, which is why tsc --init turns it on. The cost is that a mistake inside your own .d.ts files is not reported either.

Frequently Asked Questions

What is a .d.ts file in TypeScript?

A declaration file: it holds only types (function signatures, interfaces, class shapes) and no implementation. It describes JavaScript that exists somewhere else, such as a compiled library or the browser's built-in APIs, so TypeScript can check code that uses it. tsc never emits JavaScript for it.

What does the declare keyword do in TypeScript?

It tells the compiler that a value exists at run time without creating it. declare const VERSION: string; lets you use VERSION as a string, and the line disappears from the output. If nothing actually defines VERSION, the program fails at run time with a ReferenceError.

How do I fix "Could not find a declaration file for module"?

Error TS7016 means the package has no types. Install them if they exist (npm i -D @types/package-name), or add a .d.ts file with declare module "package-name" { ... } describing what you use. declare module "package-name"; alone silences the error by typing the whole module as any.

How do I generate .d.ts files from TypeScript?

Set "declaration": true in tsconfig.json (or pass --declaration). Every .ts file then produces a .d.ts next to its .js. Add emitDeclarationOnly when another tool builds the JavaScript, and declarationMap so editors can jump from the types to your source.

What is the difference between declare global and declare module?

declare global { ... } adds to the global scope, for example a new property on Array or a global variable. declare module "name" { ... } describes a module you import by that name. declare global must sit in a module (a file with an import or export); declare module declares a new module in a file without imports or exports, and augments an existing one inside a module.

Coddy programming languages illustration

Learn to code with Coddy

GET STARTED