Writing Declaration Files
TypeScript declaration files (.d.ts) let you add type information to plain JavaScript code. When you’re working with a JavaScript library that has no types, or when you want to gradually adopt TypeScript in an existing project, declaration files become essential. They describe the shape of your code without changing how it runs.
What Is a Declaration File?
A declaration file contains only type information. It tells TypeScript what types exist in your JavaScript code without adding any runtime behavior. The file ends with .d.ts and gets automatically picked up by TypeScript.
Here’s the simplest declaration file:
// greeter.js
function greet(name) {
return "Hello, " + name + "!";
}
// greeter.d.ts
declare function greet(name: string): string;
The declare keyword tells TypeScript that something exists but was defined elsewhere. You can declare functions, variables, classes, and modules.
Creating Declaration Files for a JavaScript Project
When you have a JavaScript project and want TypeScript to understand it, you have three main approaches.
Approach 1: Automatic Declaration Generation
If you’re converting a JavaScript project to TypeScript incrementally, set declare in your tsconfig.json:
{
"compilerOptions": {
"allowJs": true,
"declaration": true,
"outDir": "./dist"
}
}
With these options, TypeScript automatically generates .d.ts files for your JavaScript code during compilation.
Approach 2: Ambient Declaration Files
Create a types folder and add declaration files that TypeScript will automatically find:
// types/my-library.d.ts
declare module "my-library" {
export function calculate(a: number, b: number): number;
export const VERSION: string;
}
TypeScript looks for .d.ts files in your typeRoots or automatically in a types directory.
Approach 3: Publishing Declaration Files
If you’re publishing a library to npm, include the declaration files in your package:
{
"name": "my-library",
"types": "dist/index.d.ts"
}
When you build your TypeScript project with declaration: true, TypeScript outputs the .d.ts files to your out directory.
Extending Existing Types
Sometimes you need to add types to something that already has them. TypeScript lets you extend built-in types and third-party library types.
Module Augmentation
You can add to existing module exports:
// Extend the Express Request type
import 'express';
declare module 'express' {
export interface Request {
user?: {
id: string;
role: string;
};
}
}
Now every Request object in your Express app has an optional user property:
import { Request, Response, NextFunction } from 'express';
function authenticate(req: Request, res: Response, next: NextFunction) {
if (req.user) {
console.log(`User ${req.user.id} authenticated`);
}
next();
}
Global Augmentation
You can also extend global types that aren’t part of a module:
declare global {
interface String {
capitalize(): string;
}
}
String.prototype.capitalize = function() {
return this.charAt(0).toUpperCase() + this.slice(1);
};
This adds a capitalize() method to all strings in your codebase.
Practical Patterns
Declaration Files for Functions
When declaring function types, be specific about parameters and return types:
// Good: specific types
declare function fetchData(url: string, options?: RequestInit): Promise<any>;
// Good: overloads for different calling patterns
declare function transform(input: string): string;
declare function transform(input: number): number;
declare function transform(input: string | number): string | number;
Declaration Files for Classes
Match the JavaScript runtime behavior in your declarations:
// Point.js (runtime code)
class Point {
constructor(x, y) {
this.x = x;
this.y = y;
}
distanceTo(other) {
return Math.sqrt(
Math.pow(this.x - other.x, 2) +
Math.pow(this.y - other.y, 2)
);
}
}
// Point.d.ts
declare class Point {
constructor(x: number, y: number);
x: number;
y: number;
distanceTo(other: Point): number;
}
Declaration Files for Objects
For object literals and namespaces:
declare const config: {
apiUrl: string;
timeout: number;
retries: number;
};
declare namespace MyUtils {
function parse(input: string): object;
const VERSION: string;
}
Declaration Files for npm Packages
When creating types for an npm package you’re publishing, include the declaration file in your published package:
{
"name": "my-utils",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"files": ["dist"]
}
Build your package with TypeScript to generate both the JavaScript and declaration files:
{
"compilerOptions": {
"declaration": true,
"declarationDir": "./dist",
"outDir": "./dist"
}
}
When consumers run npm install my-utils, TypeScript automatically finds and uses the declaration file.
Common Pitfalls
Forgetting to Export
In module declaration files, everything must be explicitly exported:
// Wrong - TypeScript won't see these
declare module "my-module" {
function foo(): void; // Won't work
}
// Correct
declare module "my-module" {
export function foo(): void;
}
Mismatch Between JS and .d.ts
Your declaration file must match your JavaScript exactly:
// runtime.js
function greet(name = "World") {
return `Hello, ${name}!`;
}
// runtime.d.ts - WRONG
declare function greet(name: string): string;
// runtime.d.ts - CORRECT
declare function greet(name?: string): string;
The parameter is optional in the JS code, so it must be optional in the declaration.
Triple-Slash Directives
Older code often uses triple-slash directives to reference declaration files:
/// <reference path="./types.d.ts" />
Modern projects prefer tsconfig.json paths or typeRoots instead.
See Also
- JavaScript Modules — Understand module systems that declaration files describe
- JavaScript Symbols — Learn about JavaScript primitive types that declaration files can type
- JavaScript Proxies — Understand the Proxy API that declaration files can declare