TypeScript

Declaration Files & Type Definitions

26 min Lesson 18 of 40

Declaration Files & Type Definitions in TypeScript

Declaration files (.d.ts) are a fundamental part of the TypeScript ecosystem. They provide type information for JavaScript code, enabling type checking and IntelliSense for libraries that don't have their own TypeScript implementations. In this lesson, we'll explore how to work with declaration files and create your own type definitions.

Understanding Declaration Files

A declaration file describes the shape of an existing JavaScript module or library without containing the actual implementation. Declaration files have the .d.ts extension and contain only type information.

Basic Structure:
<// math-utils.d.ts
export function add(a: number, b: number): number;
export function subtract(a: number, b: number): number;
export function multiply(a: number, b: number): number;

export interface CalculatorOptions {
  precision?: number;
  roundingMode?: 'floor' | 'ceil' | 'round';
}

export class Calculator {
  constructor(options?: CalculatorOptions);
  calculate(expression: string): number;
  clear(): void;
}

export const PI: number;
export const E: number;
>
Note: Declaration files contain no implementation code—only type signatures, interfaces, and type definitions.

The DefinitelyTyped Repository

DefinitelyTyped is a massive community-driven repository of type definitions for thousands of JavaScript libraries. These definitions are published to npm under the @types scope.

Installing Type Definitions:
<# Install type definitions for a library
npm install --save-dev @types/lodash
npm install --save-dev @types/express
npm install --save-dev @types/node
npm install --save-dev @types/react

# Check available types
npm search @types/library-name

# Types are automatically discovered by TypeScript
# in node_modules/@types/
>
Tip: Always check if type definitions exist before writing your own. Use npm search @types/package-name or visit TypeSearch.

Writing Ambient Declarations

Ambient declarations describe types that exist elsewhere, typically in JavaScript code or external libraries. They use the declare keyword.

Ambient Declarations:
<// global.d.ts - Ambient declarations for global variables

// Declare a global variable
declare const API_URL: string;
declare const VERSION: string;

// Declare a global function
declare function gtag(
  command: 'config' | 'event',
  targetId: string,
  config?: object
): void;

// Declare a global class
declare class jQuery {
  constructor(selector: string);
  addClass(className: string): this;
  removeClass(className: string): this;
  on(event: string, handler: Function): this;
}

// Declare a global namespace
declare namespace NodeJS {
  interface ProcessEnv {
    NODE_ENV: 'development' | 'production' | 'test';
    DATABASE_URL: string;
    API_KEY: string;
  }
}

// Now these can be used without errors
console.log(API_URL);
gtag('event', 'page_view');
const $el = new jQuery('#app');
const env = process.env.NODE_ENV;
>

Module Declarations

When working with JavaScript modules that don't have type definitions, you can declare their types using module declarations.

Module Declaration:
<// declarations.d.ts

// Declare types for a third-party module
declare module 'legacy-library' {
  export function processData(data: string): any;
  export class DataProcessor {
    constructor(options?: object);
    process(input: string): string;
  }
}

// Declare wildcard module for file imports
declare module '*.css' {
  const content: { [className: string]: string };
  export default content;
}

declare module '*.png' {
  const value: string;
  export default value;
}

declare module '*.svg' {
  import React = require('react');
  export const ReactComponent: React.FC<React.SVGProps<SVGSVGElement>>;
  const src: string;
  export default src;
}

// Declare JSON modules
declare module '*.json' {
  const value: any;
  export default value;
}

// Now you can import these files
import styles from './styles.css';
import logo from './logo.png';
import config from './config.json';
>
Warning: Using wildcard module declarations with any disables type checking. Provide specific types when possible.

Creating Declaration Files for Your Library

When publishing a TypeScript library, you should generate declaration files so consumers can benefit from type checking.

tsconfig.json Configuration:
<{
  "compilerOptions": {
    "declaration": true,        // Generate .d.ts files
    "declarationMap": true,     // Generate sourcemaps for .d.ts
    "emitDeclarationOnly": false, // Also emit JS files
    "outDir": "./dist",         // Output directory
    "declarationDir": "./dist/types" // Optional: separate dir for .d.ts
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "**/*.test.ts"]
}
>
Example Library Source:
<// src/index.ts
export interface UserConfig {
  apiUrl: string;
  timeout?: number;
  retries?: number;
}

export class ApiClient {
  constructor(config: UserConfig) {
    // Implementation
  }

  async get<T>(endpoint: string): Promise<T> {
    // Implementation
    return {} as T;
  }

  async post<T>(endpoint: string, data: any): Promise<T> {
    // Implementation
    return {} as T;
  }
}

export function createClient(config: UserConfig): ApiClient {
  return new ApiClient(config);
}

// After compilation, TypeScript generates:
// dist/index.js (implementation)
// dist/index.d.ts (type declarations)
>
Generated Declaration File:
<// dist/index.d.ts (auto-generated)
export interface UserConfig {
  apiUrl: string;
  timeout?: number;
  retries?: number;
}

export declare class ApiClient {
  constructor(config: UserConfig);
  get<T>(endpoint: string): Promise<T>;
  post<T>(endpoint: string, data: any): Promise<T>;
}

export declare function createClient(config: UserConfig): ApiClient;
>

Package.json Type Configuration

Configure your package.json to point to the correct type definitions for npm package consumers.

package.json:
<{
  "name": "my-library",
  "version": "1.0.0",
  "main": "./dist/index.js",           // Entry point for CommonJS
  "module": "./dist/index.esm.js",     // Entry point for ES modules
  "types": "./dist/index.d.ts",        // Type definitions entry
  "typings": "./dist/index.d.ts",      // Alternative to "types"
  "exports": {
    ".": {
      "import": "./dist/index.esm.js",
      "require": "./dist/index.js",
      "types": "./dist/index.d.ts"
    },
    "./utils": {
      "import": "./dist/utils.esm.js",
      "require": "./dist/utils.js",
      "types": "./dist/utils.d.ts"
    }
  },
  "files": [
    "dist"
  ]
}
>
Best Practice: Use the "exports" field for modern package entry point configuration with proper type support.

Triple-Slash Directives

Triple-slash directives are single-line comments that provide compiler instructions and are only valid at the top of a file.

Triple-Slash Directives:
</// <reference path="./custom-types.d.ts" />
/// <reference types="node" />
/// <reference lib="es2020" />

// Reference another declaration file
/// <reference path="./globals.d.ts" />

// Reference @types package
/// <reference types="jquery" />

// Reference TypeScript lib
/// <reference lib="dom" />
/// <reference lib="es2021" />

// These directives tell TypeScript to include
// specific type definitions during compilation
>
Note: Triple-slash directives are mostly legacy. Modern TypeScript prefers tsconfig.json configuration and ES6 imports.

Augmenting Existing Types

You can extend existing types from libraries using declaration merging. This is useful for adding custom properties or methods.

Type Augmentation:
<// custom.d.ts

// Augment Express Request type
import { Express } from 'express';

declare global {
  namespace Express {
    interface Request {
      user?: {
        id: string;
        email: string;
        role: 'admin' | 'user';
      };
      requestId: string;
    }
  }
}

// Augment Window interface
interface Window {
  gtag: (command: string, ...args: any[]) => void;
  dataLayer: any[];
  myCustomProperty: string;
}

// Augment Array prototype
interface Array<T> {
  first(): T | undefined;
  last(): T | undefined;
}

// Now you can use these augmented types
// in your code without errors

// Express middleware
app.use((req, res, next) => {
  req.requestId = generateId(); // No error
  if (req.user) {
    console.log(req.user.role); // No error
  }
  next();
});

// Browser code
window.gtag('event', 'page_view'); // No error
console.log(window.myCustomProperty); // No error

// Array extensions
const arr = [1, 2, 3];
arr.first(); // No error
arr.last();  // No error
>
Warning: Be cautious when augmenting global types. It can lead to conflicts if multiple libraries augment the same types.

Namespace Declarations

Namespaces (formerly called "internal modules") organize code and prevent global namespace pollution in declaration files.

Namespace Declaration:
<// my-library.d.ts

declare namespace MyLibrary {
  // Interfaces
  interface Config {
    apiKey: string;
    debug?: boolean;
  }

  interface User {
    id: string;
    name: string;
  }

  // Classes
  class Client {
    constructor(config: Config);
    getUser(id: string): Promise<User>;
  }

  // Functions
  function init(config: Config): Client;
  function version(): string;

  // Nested namespace
  namespace Utils {
    function formatDate(date: Date): string;
    function parseJSON<T>(json: string): T;
  }

  // Constants
  const VERSION: string;
}

// Usage
const client = MyLibrary.init({ apiKey: 'xyz' });
const user = await client.getUser('123');
const formatted = MyLibrary.Utils.formatDate(new Date());
console.log(MyLibrary.VERSION);
>

Practical Example: Creating a Plugin Declaration

Complete Plugin Declaration:
<// types/jquery-plugin.d.ts

// Augment jQuery with custom plugin
interface JQuery {
  /**
   * My custom tooltip plugin
   * @param options - Configuration options
   */
  myTooltip(options?: MyTooltip.Options): JQuery;

  /**
   * Show the tooltip
   */
  myTooltip(action: 'show'): JQuery;

  /**
   * Hide the tooltip
   */
  myTooltip(action: 'hide'): JQuery;

  /**
   * Destroy the tooltip
   */
  myTooltip(action: 'destroy'): JQuery;
}

// Declare plugin namespace
declare namespace MyTooltip {
  interface Options {
    content?: string;
    placement?: 'top' | 'bottom' | 'left' | 'right';
    trigger?: 'hover' | 'click' | 'manual';
    delay?: number;
    animation?: boolean;
    template?: string;
    onShow?: () => void;
    onHide?: () => void;
  }

  interface API {
    show(): void;
    hide(): void;
    toggle(): void;
    destroy(): void;
    update(content: string): void;
  }

  const defaults: Options;
  const version: string;
}

// Usage examples
$('#element').myTooltip({
  content: 'Hello World',
  placement: 'top',
  trigger: 'hover'
});

$('#element').myTooltip('show');
$('#element').myTooltip('hide');

console.log(MyTooltip.version);
>

Common Declaration Patterns

Various Declaration Patterns:
<// 1. Function overloads
declare function createElement(tag: 'div'): HTMLDivElement;
declare function createElement(tag: 'span'): HTMLSpanElement;
declare function createElement(tag: string): HTMLElement;

// 2. Callable interfaces
interface ClickHandler {
  (event: MouseEvent): void;
  namespace: string;
  version: string;
}

declare const onClick: ClickHandler;

// 3. Constructor signatures
interface UserConstructor {
  new (name: string): User;
  new (name: string, email: string): User;
  readonly prototype: User;
}

declare const User: UserConstructor;

// 4. Hybrid types (callable and constructable)
interface JQueryStatic {
  (selector: string): JQuery;
  ajax(url: string, settings?: any): any;
  version: string;
}

declare const $: JQueryStatic;

// 5. Generic constraints in declarations
declare function map<T, U>(
  array: T[],
  fn: (item: T, index: number) => U
): U[];

// 6. Conditional types in declarations
declare function process<T>(
  value: T
): T extends string ? string : number;
>
Exercise: Create a declaration file for a fictional analytics library with:
  1. A global analytics object with track and identify methods
  2. Type definitions for event properties and user traits
  3. Method overloads for different parameter combinations
  4. A namespace for configuration options

Testing Your Declarations

Declaration Testing:
<// test-types.ts - Test your type declarations

import { ApiClient, UserConfig } from './index';

// Type assertions to verify types
const config: UserConfig = {
  apiUrl: 'https://api.example.com',
  timeout: 5000
};

const client = new ApiClient(config);

// This should compile without errors
(async () => {
  const user = await client.get<{ id: string }>('/user');
  const id: string = user.id; // Should be string

  await client.post('/user', { name: 'John' });
})();

// Use dtslint or tsd for automated declaration testing
// npm install --save-dev dtslint
// npm install --save-dev tsd
>
Best Practice: Use tools like tsd or dtslint to write tests for your type declarations, ensuring they work as expected.
Summary: Declaration files are essential for providing type safety in TypeScript projects. Master writing and using .d.ts files to work effectively with JavaScript libraries and publish type-safe packages.