Writing Your Own Declaration Files
When you use a JavaScript library that has no @types package, or when you expose a TypeScript library to consumers, you need to write your own .d.ts declaration files. Declaration files describe the shape of JavaScript code — they contain no runtime logic, only types.
This page covers everything from a minimal stub to a professional-grade declaration file.
What Is a Declaration File?
A declaration file (extension .d.ts) tells TypeScript about the types of values that exist outside its direct visibility — usually in plain JavaScript packages or globally injected scripts.
Key facts:
- Declaration files are never compiled to JavaScript — they exist only at build time.
- They use the
declarekeyword for all top-level statements. - They can describe modules, global variables, functions, classes, and namespaces.
// example.d.ts
// Declare a variable that exists globally (e.g., injected by a script tag)
declare const APP_VERSION: string;
// Declare a function that exists globally
declare function greet(name: string): void;
// Declare a class available globally
declare class EventBus {
on(event: string, handler: () => void): void;
emit(event: string): void;
}The declare Keyword
declare tells TypeScript: "this thing exists at runtime, but I'm not going to show you its implementation here." Every statement in a .d.ts file that would normally produce JavaScript output must be prefixed with declare.
Normal TypeScript | Declaration equivalent |
|---|---|
const x = 42 | declare const x: number |
function f(n: number): string { ... } | declare function f(n: number): string |
class Foo { ... } | declare class Foo { ... } |
namespace NS { ... } | declare namespace NS { ... } |
enum Color { Red } | declare enum Color { Red } |
declare namespace or declare module block, the declare keyword on inner members is optional — all members are implicitly ambient.Quickest Start: A Minimal Stub
If you just want to silence TypeScript errors for a module without writing a full declaration, create a stub:
// src/types/legacy-lib.d.ts declare module 'legacy-lib'; // This types every import from 'legacy-lib' as 'any'. // It silences errors while you write the real declarations.
Declaring a CommonJS Module
Most npm packages expose a CommonJS API. Declare them with declare module 'name' { ... } and export a value using export =.
// types/slugify.d.ts
declare module 'slugify' {
interface SlugifyOptions {
replacement?: string; // default: '-'
remove?: RegExp; // remove matched characters
lower?: boolean; // lowercase the result
strict?: boolean; // strip special characters
locale?: string; // locale for transliteration
trim?: boolean; // trim leading/trailing replacement chars
}
function slugify(string: string, options?: SlugifyOptions | string): string;
namespace slugify {
function extend(characters: Record<string, string>): void;
}
export = slugify;
}
// Usage — now fully typed
import slugify from 'slugify';
slugify('Hello World!', { lower: true }); // 'hello-world'Declaring an ES Module
For packages that use ES module exports, use regular export statements inside the declare module block.
// types/color-utils.d.ts
declare module 'color-utils' {
export interface RGB {
r: number;
g: number;
b: number;
}
export interface HSL {
h: number;
s: number;
l: number;
}
export function hexToRgb(hex: string): RGB;
export function rgbToHsl(rgb: RGB): HSL;
export function lighten(color: string, amount: number): string;
export function darken(color: string, amount: number): string;
export const VERSION: string;
}
// Usage
import { hexToRgb, lighten } from 'color-utils';
const rgb = hexToRgb('#ff6b6b'); // { r: 255, g: 107, b: 107 }Declaring Global Variables (Script-Injected)
When a script tag injects a variable into the global scope (common with analytics libraries, A/B testing tools, or CDN-hosted libraries), declare it globally in a .d.ts file.
// src/types/globals.d.ts
// Must be a script file (no top-level import/export) to add globals,
// OR use 'declare global { }' inside a module file.
declare const gtag: (
command: 'config' | 'event' | 'set' | 'js',
targetId: string | Date,
params?: Record<string, unknown>,
) => void;
declare const dataLayer: Record<string, unknown>[];
declare namespace analytics {
function track(event: string, properties?: Record<string, unknown>): void;
function identify(userId: string, traits?: Record<string, unknown>): void;
function page(name?: string): void;
}// Alternative: declare global {} inside a module
// src/types/globals.d.ts
export {}; // makes this file a module
declare global {
const gtag: (
command: 'config' | 'event' | 'set' | 'js',
targetId: string | Date,
params?: Record<string, unknown>,
) => void;
const dataLayer: Record<string, unknown>[];
}Structuring a Full Library Declaration
Real libraries have classes, interfaces, functions, and constants. Here's how to write a thorough declaration file for a hypothetical HTTP client library.
// types/http-client.d.ts
declare module 'http-client' {
// ─── Configuration ───────────────────────────────────────────
export interface HttpClientConfig {
baseURL?: string;
timeout?: number;
headers?: Record<string, string>;
retries?: number;
}
// ─── Request / Response ───────────────────────────────────────
export interface RequestConfig extends HttpClientConfig {
method?: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
params?: Record<string, string | number | boolean>;
data?: unknown;
}
export interface HttpResponse<T = unknown> {
data: T;
status: number;
statusText: string;
headers: Record<string, string>;
config: RequestConfig;
}
// ─── Interceptors ─────────────────────────────────────────────
export interface InterceptorManager<T> {
use(
onFulfilled?: (value: T) => T | Promise<T>,
onRejected?: (error: unknown) => unknown,
): number;
eject(id: number): void;
}
// ─── Main Class ───────────────────────────────────────────────
export class HttpClient {
constructor(config?: HttpClientConfig);
interceptors: {
request: InterceptorManager<RequestConfig>;
response: InterceptorManager<HttpResponse>;
};
get<T = unknown>(url: string, config?: RequestConfig): Promise<HttpResponse<T>>;
post<T = unknown>(url: string, data?: unknown, config?: RequestConfig): Promise<HttpResponse<T>>;
put<T = unknown>(url: string, data?: unknown, config?: RequestConfig): Promise<HttpResponse<T>>;
patch<T = unknown>(url: string, data?: unknown, config?: RequestConfig): Promise<HttpResponse<T>>;
delete<T = unknown>(url: string, config?: RequestConfig): Promise<HttpResponse<T>>;
static create(config?: HttpClientConfig): HttpClient;
}
// ─── Factory function (default export) ───────────────────────
function createClient(config?: HttpClientConfig): HttpClient;
export default createClient;
}Overloaded Function Declarations
Declare multiple signatures for functions that behave differently based on argument types. List the most-specific overloads first.
// types/query-selector.d.ts
declare module 'query-selector' {
// Overloads — narrow types returned based on the selector string
function $(selector: '#app'): HTMLDivElement | null;
function $(selector: 'input'): HTMLInputElement | null;
function $(selector: string): HTMLElement | null;
// Overloads on second argument
function $(selector: string, context: Document): HTMLElement | null;
function $(selector: string, context: HTMLElement): HTMLElement | null;
export = $;
}Generics in Declaration Files
Generic declarations work exactly the same as in regular TypeScript — they just use declare syntax.
// types/event-emitter.d.ts
declare module 'typed-emitter' {
type EventMap = Record<string, (...args: unknown[]) => void>;
export class TypedEmitter<Events extends EventMap> {
on<K extends keyof Events>(event: K, listener: Events[K]): this;
off<K extends keyof Events>(event: K, listener: Events[K]): this;
once<K extends keyof Events>(event: K, listener: Events[K]): this;
emit<K extends keyof Events>(event: K, ...args: Parameters<Events[K]>): boolean;
removeAllListeners<K extends keyof Events>(event?: K): this;
listenerCount<K extends keyof Events>(event: K): number;
}
}
// Usage
import { TypedEmitter } from 'typed-emitter';
interface AppEvents {
login: (userId: string) => void;
logout: () => void;
error: (err: Error) => void;
}
const emitter = new TypedEmitter<AppEvents>();
emitter.on('login', (userId) => console.log('Logged in:', userId));
emitter.emit('login', 'user-123'); // type-safe!Configuring tsconfig to Find Your Declarations
TypeScript needs to know where your custom .d.ts files live. The two common setups are:
// tsconfig.json — Option 1: include in typeRoots
{
"compilerOptions": {
"typeRoots": ["./node_modules/@types", "./src/types"]
}
}
// tsconfig.json — Option 2: include via paths glob
{
"compilerOptions": {
"baseUrl": "."
},
"include": ["src/**/*", "types/**/*"]
}src/types/ (for app-specific augmentations) or types/ at the project root (for third-party stubs). Add both to your tsconfig include array.Generating Declarations From Your Own TypeScript
If you're writing a TypeScript library, let the compiler generate declaration files automatically. Enable declaration: true in your tsconfig.json.
// tsconfig.json for a library
{
"compilerOptions": {
"declaration": true, // emit .d.ts files
"declarationDir": "dist", // where to put them
"declarationMap": true, // emit .d.ts.map for source navigation
"emitDeclarationOnly": true // only emit .d.ts, skip JS (if bundler handles JS)
}
}# After building, your dist/ folder contains: # dist/index.js # dist/index.d.ts ← declaration file # dist/index.d.ts.map ← source map for the declaration
// package.json — tell consumers where to find types
{
"main": "dist/index.js",
"types": "dist/index.d.ts",
"exports": {
".": {
"import": "./dist/index.mjs",
"require": "./dist/index.js",
"types": "./dist/index.d.ts"
}
}
}types field in package.json, consumers get full type safety automatically — no @types package needed.Best Practices
Start with a minimal stub to unblock development, then refine types incrementally.
Prefer interfaces over type aliases in declaration files — interfaces merge, aliases do not.
Use generics to express relationships between argument and return types.
Keep one declaration file per third-party library, named after the library.
Test your declarations by writing code that uses them and verifying TypeScript is happy.
Enable "strict": true in the tsconfig used by your declaration tests.
Use export = for CommonJS modules and named exports for ES modules.
Add JSDoc comments to your declarations — they appear in editor hover tooltips.
Summary
Declaration files (.d.ts) describe JavaScript APIs using the declare keyword — no runtime logic.
Use declare module for library declarations and declare global for globally-injected values.
A wildcard declare module silences errors quickly; write real types as a follow-up.
Overloaded functions list the most-specific signatures first.
Generics in declarations work the same as in regular TypeScript.
Enable declaration: true in tsconfig to auto-generate .d.ts from your own TypeScript source.
Set the types field in package.json so consumers find your declarations automatically.