Declaration Files & Type Definitions
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.
<// 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;
>
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.
<# 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/
>
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.
<// 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.
<// 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';
>
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.
<{
"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"]
}
>
<// 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)
>
<// 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.
<{
"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"
]
}
>
"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.
</// <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
>
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.
<// 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
>
Namespace Declarations
Namespaces (formerly called "internal modules") organize code and prevent global namespace pollution in declaration files.
<// 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
<// 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
<// 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;
>
- A global
analyticsobject withtrackandidentifymethods - Type definitions for event properties and user traits
- Method overloads for different parameter combinations
- A namespace for configuration options
Testing Your Declarations
<// 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
>
tsd or dtslint to write tests for your type declarations, ensuring they work as expected.