Writing .d.ts Files
Declaration files (files with the .d.ts extension) describe the shape of existing JavaScript to the TypeScript compiler without providing any runtime code. Every npm package that ships types — including TypeScript's own standard library — delivers its public API through declaration files.
What Are .d.ts Files?
A .d.ts file contains only ambient declarations: types, interfaces, function signatures, and variable declarations prefixed with the declare keyword. They emit no JavaScript.
// math-utils.d.ts
declare function add(a: number, b: number): number;
declare const PI: number;The declare Keyword
declare tells TypeScript "trust me, this exists at runtime." Use it for:
declare const/declare let/declare var— global variablesdeclare function— global functionsdeclare class— global classesdeclare namespace— grouped globals (legacy pattern)declare module— whole module shapes
declare module
Use declare module to type a JavaScript module that has no types of its own:
// legacy-lib.d.ts
declare module "legacy-lib" {
export function doThing(x: string): void;
export const version: string;
}declare global
Inside a module file (any file with an import or export), use declare global to add or extend global scope:
export {}; // makes this a module
declare global {
interface Window {
analytics: AnalyticsInstance;
}
}Triple-Slash Directives
Before ES module imports existed, /// <reference> directives linked declaration files together. They are still useful for referencing types packages and lib files:
/// <reference types="node" />
/// <reference path="./custom-globals.d.ts" />When to Write vs. Use @types
- If a library ships its own types (check its
package.jsonfor a"types"or"typings"field), no extra work is needed. - If DefinitelyTyped has a package (
@types/lodash), install that. - Write a custom
.d.tsfile only for in-house JS that will never be published, or to patch incomplete community types.
Understanding .d.ts files demystifies what npm packages are actually delivering and gives you the tools to fill gaps in third-party type coverage.
Code Examples
// File: src/types/legacy-analytics.d.ts
// Provides types for the untyped "legacy-analytics" npm package
declare module "legacy-analytics" {
export interface TrackOptions {
userId: string;
event: string;
properties?: Record<string, unknown>;
}
export function track(options: TrackOptions): void;
export function identify(userId: string, traits?: Record<string, unknown>): void;
export function page(name: string): void;
export const version: string;
// Default export — the analytics singleton
const analytics: {
track: typeof track;
identify: typeof identify;
page: typeof page;
};
export default analytics;
}
// ----- Usage in application code -----
// Now TypeScript fully understands "legacy-analytics"
import analytics, { track } from "legacy-analytics";
track({ userId: "u-1", event: "signup" });
console.log("Module declaration working correctly");The declare module block gives TypeScript a complete type signature for a JavaScript package that ships no .d.ts files. After creating this file, all imports from that package are fully type-checked.
// File: src/types/express-augment.d.ts
// Augments Express's Request type to include our custom user property
import "express";
declare module "express" {
interface Request {
currentUser?: {
id: string;
role: "admin" | "user" | "guest";
};
}
}
// ----- Simulated usage -----
// In a real Express app you would import { Request, Response } from "express"
// Here we demonstrate the shape of the augmented interface
interface Request {
currentUser?: {
id: string;
role: "admin" | "user" | "guest";
};
path: string;
}
function authMiddleware(req: Request): void {
if (!req.currentUser) {
console.log("Unauthenticated request to", req.path);
return;
}
console.log(`User ${req.currentUser.id} (${req.currentUser.role}) accessing ${req.path}`);
}
authMiddleware({ path: "/dashboard", currentUser: { id: "u-99", role: "admin" } });
authMiddleware({ path: "/login" });Module augmentation extends an existing type declaration without forking it. The import at the top makes this a module file, and declare module 'express' opens the existing declaration for extension.
// File: src/types/globals.d.ts
// Declares globals injected by the build tool (e.g., Webpack DefinePlugin)
declare const __APP_VERSION__: string;
declare const __BUILD_DATE__: string;
declare const __DEV__: boolean;
// Extend the built-in Window interface
declare global {
interface Window {
dataLayer: Array<Record<string, unknown>>;
gtag: (...args: unknown[]) => void;
}
}
// Extend NodeJS global (useful in Node environments)
declare namespace NodeJS {
interface ProcessEnv {
DATABASE_URL: string;
JWT_SECRET: string;
NODE_ENV: "development" | "production" | "test";
}
}
// ----- Simulated runtime values -----
const appVersion = __APP_VERSION__;
const buildDate = __BUILD_DATE__;
const isDev = __DEV__;
console.log(`App ${appVersion} built on ${buildDate}`);
console.log(`Dev mode: ${isDev}`);Global ambient declarations tell TypeScript about variables injected at build time or attached to global objects. declare global inside a module file safely augments the global scope without polluting other modules.
Quick Quiz
1. What keyword prefix is used for all declarations inside a .d.ts file?
2. What must you add to make declare global work inside a module file?
3. Which triple-slash directive is used to reference a @types package?
4. When should you write a custom .d.ts file instead of using @types?
Was this lesson helpful?