Back to Blog
TypeScriptTypesDeclaration FilesLibraries

Writing TypeScript Declaration Files

Create type definitions for JavaScript libraries. From basic declarations to module augmentation to publishing on DefinitelyTyped.

B
Bootspring Team
Engineering
February 12, 2022
6 min read

Declaration files (.d.ts) provide type information for JavaScript libraries. Here's how to write them effectively.

Basic Declaration Structure#

1// types/my-library.d.ts 2 3// Declare module for npm package 4declare module 'my-library' { 5 // Export types 6 export interface Config { 7 apiKey: string; 8 timeout?: number; 9 debug?: boolean; 10 } 11 12 // Export functions 13 export function initialize(config: Config): void; 14 export function getData(id: string): Promise<Data>; 15 16 // Export classes 17 export class Client { 18 constructor(config: Config); 19 connect(): Promise<void>; 20 disconnect(): void; 21 send(message: string): Promise<Response>; 22 } 23 24 // Export type aliases 25 export type Status = 'pending' | 'active' | 'completed'; 26 27 // Default export 28 const defaultExport: { 29 version: string; 30 Client: typeof Client; 31 initialize: typeof initialize; 32 }; 33 34 export default defaultExport; 35}

Global Declarations#

1// types/globals.d.ts 2 3// Declare global variable 4declare const CONFIG: { 5 apiUrl: string; 6 environment: string; 7}; 8 9// Declare global function 10declare function gtag( 11 command: 'config' | 'event', 12 targetId: string, 13 config?: Record<string, any> 14): void; 15 16// Extend Window interface 17declare global { 18 interface Window { 19 analytics: { 20 track: (event: string, data?: Record<string, any>) => void; 21 identify: (userId: string) => void; 22 }; 23 __INITIAL_STATE__: any; 24 } 25} 26 27// Extend existing module 28declare module 'express' { 29 interface Request { 30 user?: { 31 id: string; 32 email: string; 33 role: string; 34 }; 35 } 36} 37 38// Ambient declaration (no import needed) 39declare namespace NodeJS { 40 interface ProcessEnv { 41 NODE_ENV: 'development' | 'production' | 'test'; 42 DATABASE_URL: string; 43 API_KEY: string; 44 } 45} 46 47export {}; // Make this a module

Module Augmentation#

1// Extend third-party library types 2 3// Extend React 4import 'react'; 5 6declare module 'react' { 7 interface CSSProperties { 8 // Allow CSS custom properties 9 [key: `--${string}`]: string | number; 10 } 11} 12 13// Extend express-session 14import 'express-session'; 15 16declare module 'express-session' { 17 interface SessionData { 18 userId: string; 19 cart: CartItem[]; 20 preferences: UserPreferences; 21 } 22} 23 24// Extend prisma client 25import { PrismaClient } from '@prisma/client'; 26 27declare module '@prisma/client' { 28 interface PrismaClient { 29 $extends: any; 30 } 31} 32 33// Add custom matchers to Jest 34declare global { 35 namespace jest { 36 interface Matchers<R> { 37 toBeWithinRange(floor: number, ceiling: number): R; 38 toHaveBeenCalledWithError(error: Error): R; 39 } 40 } 41}

Function Overloads#

1// types/utils.d.ts 2 3// Function with multiple signatures 4declare function format(value: string): string; 5declare function format(value: number, decimals?: number): string; 6declare function format(value: Date, pattern?: string): string; 7 8// Generic function 9declare function pick<T, K extends keyof T>(obj: T, keys: K[]): Pick<T, K>; 10 11// Callback-style function 12declare function fetchData( 13 url: string, 14 callback: (error: Error | null, data: any) => void 15): void; 16 17// Promise-style function 18declare function fetchData(url: string): Promise<any>; 19 20// Options object pattern 21declare function request( 22 url: string, 23 options?: { 24 method?: 'GET' | 'POST' | 'PUT' | 'DELETE'; 25 headers?: Record<string, string>; 26 body?: any; 27 timeout?: number; 28 } 29): Promise<Response>;

Class Declarations#

1// types/classes.d.ts 2 3declare class EventEmitter { 4 // Constructor overloads 5 constructor(); 6 constructor(options: { maxListeners: number }); 7 8 // Methods 9 on(event: string, listener: (...args: any[]) => void): this; 10 once(event: string, listener: (...args: any[]) => void): this; 11 off(event: string, listener: (...args: any[]) => void): this; 12 emit(event: string, ...args: any[]): boolean; 13 14 // Static members 15 static defaultMaxListeners: number; 16 17 // Protected/private 18 protected listeners: Map<string, Function[]>; 19} 20 21// Generic class 22declare class Repository<T extends { id: string }> { 23 constructor(collection: string); 24 25 find(id: string): Promise<T | null>; 26 findAll(filter?: Partial<T>): Promise<T[]>; 27 create(data: Omit<T, 'id'>): Promise<T>; 28 update(id: string, data: Partial<T>): Promise<T>; 29 delete(id: string): Promise<boolean>; 30} 31 32// Abstract class 33declare abstract class BaseService { 34 abstract initialize(): Promise<void>; 35 abstract shutdown(): Promise<void>; 36 37 protected logger: Logger; 38 readonly name: string; 39}

Namespace Declarations#

1// types/namespace.d.ts 2 3declare namespace MyLib { 4 // Nested types 5 interface Config { 6 debug: boolean; 7 } 8 9 namespace Utils { 10 function formatDate(date: Date): string; 11 function parseDate(str: string): Date; 12 } 13 14 // Callable namespace 15 function init(config: Config): void; 16 17 // Nested namespace 18 namespace Http { 19 interface RequestOptions { 20 method: string; 21 url: string; 22 } 23 24 function request(options: RequestOptions): Promise<Response>; 25 } 26} 27 28// UMD module pattern 29export = MyLib; 30export as namespace MyLib; 31 32// Usage in browser (UMD) 33// MyLib.init({ debug: true }); 34// MyLib.Utils.formatDate(new Date()); 35 36// Usage in Node.js 37// import MyLib from 'my-lib'; 38// MyLib.init({ debug: true });

Conditional Types in Declarations#

1// types/conditional.d.ts 2 3// Conditional return type 4declare function getValue<T extends 'string' | 'number' | 'boolean'>( 5 type: T 6): T extends 'string' 7 ? string 8 : T extends 'number' 9 ? number 10 : boolean; 11 12// Infer from options 13interface Options { 14 async?: boolean; 15} 16 17declare function process<T extends Options>( 18 data: string, 19 options?: T 20): T extends { async: true } ? Promise<Result> : Result; 21 22// Mapped types 23type Getters<T> = { 24 [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K]; 25}; 26 27declare function createGetters<T extends object>(obj: T): Getters<T>; 28 29// Template literal types 30type EventName<T extends string> = `on${Capitalize<T>}`; 31 32declare function addHandler<T extends string>( 33 event: T, 34 handler: (event: EventName<T>) => void 35): void;

Third-Party Library Example#

1// types/analytics-lib.d.ts 2 3declare module 'analytics-lib' { 4 export interface AnalyticsConfig { 5 writeKey: string; 6 debug?: boolean; 7 plugins?: Plugin[]; 8 } 9 10 export interface Plugin { 11 name: string; 12 initialize: (config: AnalyticsConfig) => void; 13 track: (event: TrackEvent) => void; 14 identify: (traits: UserTraits) => void; 15 } 16 17 export interface TrackEvent { 18 event: string; 19 properties?: Record<string, any>; 20 timestamp?: Date; 21 } 22 23 export interface UserTraits { 24 userId: string; 25 email?: string; 26 name?: string; 27 [key: string]: any; 28 } 29 30 export interface Analytics { 31 track(event: string, properties?: Record<string, any>): void; 32 identify(userId: string, traits?: Omit<UserTraits, 'userId'>): void; 33 page(name?: string, properties?: Record<string, any>): void; 34 group(groupId: string, traits?: Record<string, any>): void; 35 reset(): void; 36 } 37 38 export function createAnalytics(config: AnalyticsConfig): Analytics; 39 40 const analytics: Analytics; 41 export default analytics; 42}

Publishing Types#

1// package.json for type package 2{ 3 "name": "@types/my-library", 4 "version": "1.0.0", 5 "types": "index.d.ts", 6 "typesVersions": { 7 ">=4.0": { "*": ["ts4.0/*"] }, 8 "*": { "*": ["ts3.4/*"] } 9 }, 10 "files": [ 11 "index.d.ts", 12 "ts4.0/", 13 "ts3.4/" 14 ] 15} 16 17// tsconfig.json for type package 18{ 19 "compilerOptions": { 20 "module": "commonjs", 21 "lib": ["es6"], 22 "noImplicitAny": true, 23 "noImplicitThis": true, 24 "strictNullChecks": true, 25 "strictFunctionTypes": true, 26 "types": [], 27 "noEmit": true, 28 "forceConsistentCasingInFileNames": true 29 }, 30 "files": ["index.d.ts"] 31}

Best Practices#

Structure: ✓ Mirror the library's API structure ✓ Use namespaces for grouping ✓ Export all public types ✓ Document with JSDoc comments Accuracy: ✓ Match actual library behavior ✓ Use strict type checking ✓ Handle edge cases ✓ Test with real usage Compatibility: ✓ Support multiple TypeScript versions ✓ Avoid bleeding-edge features ✓ Provide ambient and module forms ✓ Include UMD support if needed Maintenance: ✓ Version alongside library ✓ Include type tests ✓ Document breaking changes ✓ Accept contributions

Conclusion#

Declaration files bridge JavaScript libraries to TypeScript. Match the library's structure, use appropriate declaration forms (module, namespace, global), and test thoroughly. For popular libraries, consider contributing to DefinitelyTyped. Good type definitions make libraries much more usable in TypeScript projects.

Share this article

Help spread the word about Bootspring