.d.ts cheatsheet.
What is a .d.ts
Pure type information — no runtime code. Used to describe:
- existing JS libraries
- global ambient types
- non-code asset imports (CSS, SVG)
- environment variables
Ambient module
// types/legacy-lib.d.ts
declare module "legacy-lib" {
export function doThing(s: string): number;
export const VERSION: string;
}
Now import { doThing } from "legacy-lib" typechecks.
Wildcard ambient module
declare module "*.svg" {
const content: string;
export default content;
}
declare module "*.module.css" {
const classes: { readonly [key: string]: string };
export default classes;
}
Global declarations
// global.d.ts
declare const VERSION: string;
declare function gtag(...args: any[]): void;
interface Window {
myApp?: { version: string };
}
Available everywhere without import.
To make a file a module (and isolate declarations), add export {}:
// types/foo.d.ts
export {};
declare global {
const VERSION: string;
}
Module augmentation
import "express";
declare module "express" {
interface Request {
user?: { id: number };
}
}
import "react";
declare module "react" {
interface CSSProperties {
"--my-var"?: string;
}
}
process.env types
declare global {
namespace NodeJS {
interface ProcessEnv {
DATABASE_URL: string;
NODE_ENV: "development" | "production" | "test";
PORT?: string;
}
}
}
export {};
Now process.env.DATABASE_URL is string, not string | undefined.
Generating declarations from source
tsconfig.json:
{
"compilerOptions": {
"declaration": true,
"declarationMap": true,
"emitDeclarationOnly": false,
"outDir": "dist"
}
}
tsc
Emits .d.ts next to .js.
emitDeclarationOnly + esbuild
{ "emitDeclarationOnly": true, "outDir": "dist" }
tsc # types only
esbuild src/index.ts ... # JS only (faster)
Or use tsup/unbuild to do both.
triple-slash directives
/// <reference types="node" />
/// <reference lib="dom" />
/// <reference path="./other.d.ts" />
Use only in .d.ts. Avoid in source files — prefer tsconfig.json lib/types.
namespace (legacy)
declare namespace Foo {
export function bar(): void;
export const baz: string;
}
Foo.bar();
Used in old declaration files. For new code, prefer modules.
Declaration merging
Interfaces with the same name merge:
interface Box { x: number; }
interface Box { y: number; }
// Box now has x and y
Classes + namespace + interface merge in specific patterns:
class Albums { ... }
namespace Albums {
export const VERSION = "1";
}
Albums.VERSION;
Function with properties
declare function format(s: string): string;
declare namespace format {
function plural(n: number, s: string): string;
}
format("hi");
format.plural(2, "cats");
Reading existing types
Check node_modules/@types/<pkg> or node_modules/<pkg>/dist/*.d.ts for the published types.
DefinitelyTyped
npm i -D @types/lodash @types/node
Community types for libraries that don’t ship their own.
Stubbing missing types
Quick stub for a no-types library:
declare module "untyped-lib";
Imports as any. Tighten later by writing real types.
skipLibCheck
{ "skipLibCheck": true }
Don’t check types of .d.ts files in node_modules. Saves a lot of time. Recommended.
Publishing types
package.json:
{
"types": "./dist/index.d.ts",
"exports": {
".": {
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
}
}
}
Put "types" first in each conditional block — TS picks the first match.
Common mistakes
.d.tswith import statements becomes a module — globals stop working withoutdeclare global.interface Window { ... }only merges if the file is a module.- Triple-slash refs in source files — confusing; use tsconfig.
- Stale published types — bump after every API change.
- Mixing
namespaceand ES modules — pick one.
Read this next
If you want my .d.ts library template, it’s at rajpoot.dev .
Building something AI-, backend-, or data-heavy and want a second pair of eyes? I do consulting and freelance work — see my projects and ways to reach me at rajpoot.dev .