TypeScriptWriting Your Own Declarations

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 declare keyword for all top-level statements.
  • They can describe modules, global variables, functions, classes, and namespaces.

TS
// 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 }

Note
Inside a 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:

TS
// 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.
Warning
A wildcard stub like this loses all type safety — use it temporarily, not permanently.
Declaring a CommonJS Module

Most npm packages expose a CommonJS API. Declare them with declare module 'name' { ... } and export a value using export =.

TS
// 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.

TS
// 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.

TS
// 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;
}

TS
// 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.

TS
// 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.

TS
// 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.

TS
// 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:

JSON
// 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/**/*"]
}
Tip
Keep custom declarations in 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.

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)
  }
}

Bash
# 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

JSON
// 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"
    }
  }
}
Success
When you publish your library to npm with a 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.