Declaration Files: Writing .d.ts and Publishing @types
What Declaration Files Are
.d.ts files are type-only files — they contain nothing but type declarations, no runtime code, and they produce no JavaScript output. Their job is to tell TypeScript the type shape of a JavaScript module or global variable, enabling type checking and autocomplete when you use it.
// math-utils.js (JavaScript library, no types)
function add(a, b) { return a + b; }
function clamp(value, min, max) { return Math.min(Math.max(value, min), max); }
module.exports = { add, clamp };
// math-utils.d.ts (corresponding declaration file)
export declare function add(a: number, b: number): number;
export declare function clamp(value: number, min: number, max: number): number;
TypeScript looks for .d.ts files in this order:
- Path specified by
typesortypingsfield inpackage.json index.d.tsin the package rootnode_modules/@types/package-name/index.d.ts(from DefinitelyTyped)
Global Declarations
When a JS library injects global variables via <script> tags, global declarations tell TypeScript those globals exist.
// globals.d.ts
// Global variable
declare var __DEV__: boolean;
declare var __VERSION__: string;
// Global function
declare function log(message: string, level?: "info" | "warn" | "error"): void;
declare function require(module: string): unknown;
// Global class
declare class EventEmitter {
on(event: string, listener: (...args: unknown[]) => void): this;
off(event: string, listener: (...args: unknown[]) => void): this;
emit(event: string, ...args: unknown[]): boolean;
}
// Global namespace (for globals with sub-properties)
declare namespace MyApp {
interface Config {
apiUrl: string;
timeout: number;
}
function init(config: Config): void;
function destroy(): void;
}
Usage:
// No import needed — use directly
if (__DEV__) {
log("Debug mode active", "info");
}
MyApp.init({ apiUrl: "https://api.example.com", timeout: 5000 });
Module Declarations
To type a JavaScript module, use declare module "module-name" syntax.
Basic module declaration
// Declaring types for "lodash" (simplified — actual @types/lodash is much larger)
declare module "lodash" {
export function chunk<T>(array: T[], size?: number): T[][];
export function flatten<T>(array: (T | T[])[]): T[];
export function debounce<T extends (...args: unknown[]) => unknown>(
func: T,
wait?: number
): T;
// Default export
const _: {
chunk: typeof chunk;
flatten: typeof flatten;
debounce: typeof debounce;
};
export default _;
}
Wildcard module declarations
Handle CSS modules, image files, SVGs, and other non-JS assets:
// Allow TypeScript to accept .css module imports
declare module "*.css" {
const styles: { [className: string]: string };
export default styles;
}
// SVG as a React component (Vite's ?react suffix)
declare module "*.svg?react" {
import React from "react";
const SVGComponent: React.FC<React.SVGProps<SVGSVGElement>>;
export default SVGComponent;
}
// Image assets
declare module "*.png" {
const src: string;
export default src;
}
declare module "*.json" {
const value: unknown;
export default value;
}
Augmenting Existing Types (Module Augmentation)
You don't need to replace existing type declarations — you can add to them. This is the standard way to attach custom properties to framework objects.
Augmenting Express's Request object
// src/types/express.d.ts
import "express";
declare module "express" {
interface Request {
user?: {
id: string;
email: string;
roles: string[];
};
requestId?: string;
startTime?: number;
}
}
Using it in middleware:
// middleware/auth.ts
import { Request, Response, NextFunction } from "express";
import { verifyToken } from "./jwt";
export function authMiddleware(req: Request, res: Response, next: NextFunction) {
const token = req.headers.authorization?.split(" ")[1];
if (!token) return res.status(401).json({ error: "Unauthorized" });
req.user = verifyToken(token); // correctly typed, no need for as any
next();
}
Augmenting global namespaces
// Extend ProcessEnv with known environment variable types
declare namespace NodeJS {
interface ProcessEnv {
NODE_ENV: "development" | "production" | "test";
DATABASE_URL: string;
PORT?: string;
}
}
Triple-Slash References
Reference other type dependencies inside declaration files using /// <reference .../> syntax.
// Reference a types package
/// <reference types="node" />
/// <reference types="jest" />
// Reference another declaration file
/// <reference path="./globals.d.ts" />
In modern TypeScript projects, triple-slash references are mainly used in two cases:
- Global declaration files that need to reference other type libraries
- A library's
.d.tsentry file that aggregates multiple sub-declaration files
For most situations, using the types field in tsconfig.json or plain import statements is sufficient.
Practical Example: Writing a .d.ts for a Real JS Library
Using a simple utility library string-utils as a step-by-step example.
The JavaScript library (string-utils/index.js):
"use strict";
function truncate(str, maxLength, ellipsis) {
ellipsis = ellipsis || "...";
if (str.length <= maxLength) return str;
return str.slice(0, maxLength - ellipsis.length) + ellipsis;
}
function slugify(str) {
return str
.toLowerCase()
.replace(/[^\w\s-]/g, "")
.replace(/[\s_-]+/g, "-")
.replace(/^-+|-+$/g, "");
}
function template(str, vars) {
return str.replace(/\{\{(\w+)\}\}/g, (_, key) => {
return vars[key] !== undefined ? String(vars[key]) : "";
});
}
module.exports = { truncate, slugify, template };
Step 1: Analyze the export structure
This library uses CommonJS module.exports, with named exports accessible both as properties and as the whole object.
Step 2: Write the basic function signatures
// string-utils/index.d.ts (draft)
export declare function truncate(
str: string,
maxLength: number,
ellipsis?: string
): string;
export declare function slugify(str: string): string;
export declare function template(
str: string,
vars: Record<string, string | number | boolean>
): string;
Step 3: Handle CommonJS compatibility
Because this library uses module.exports, TypeScript in CommonJS projects needs special handling:
// string-utils/index.d.ts (complete version, ESM-style named exports)
export declare function truncate(str: string, maxLength: number, ellipsis?: string): string;
export declare function slugify(str: string): string;
export declare function template(str: string, vars: Record<string, string | number | boolean>): string;
// For projects using require() directly, you can add:
// export = { truncate, slugify, template };
// But export = cannot be mixed with named exports — pick one
For a purely CommonJS library, use export =:
// CommonJS-style declaration
interface StringUtils {
truncate(str: string, maxLength: number, ellipsis?: string): string;
slugify(str: string): string;
template(str: string, vars: Record<string, string | number | boolean>): string;
}
declare const stringUtils: StringUtils;
export = stringUtils;
// Usage (with esModuleInterop: true):
// import stringUtils from "string-utils";
// const { truncate } = stringUtils;
Step 4: Point package.json at the types
{
"name": "string-utils",
"version": "1.0.0",
"main": "index.js",
"types": "index.d.ts"
}
export = vs export default
This is the most commonly confused area in declaration files.
// export = : CommonJS style, equivalent to module.exports = xxx
// Declaration file
declare function createServer(options: ServerOptions): Server;
export = createServer;
// Usage
import createServer = require("my-server"); // canonical form
import createServer from "my-server"; // requires esModuleInterop: true
// export default: ES Module style
// Declaration file
declare function createServer(options: ServerOptions): Server;
export default createServer;
// Usage
import createServer from "my-server"; // normal ESM import
Decision rule:
- Library uses
module.exports = xxx: useexport = - Library uses
export default xxx: useexport default - Library uses
module.exports = xxxwith sub-properties likemodule.exports.foo = bar: useexport =with an interface declaration
Publishing to DefinitelyTyped
If a library you use has no types, you can write declaration files and publish them to DefinitelyTyped, making them installable by everyone via @types/xxx.
Directory structure:
DefinitelyTyped/
└── types/
└── string-utils/
├── index.d.ts # main declaration file
├── index.d.ts.map # source map (optional)
├── package.json # package metadata
└── tsconfig.json # used for testing
types/string-utils/package.json:
{
"private": true,
"name": "@types/string-utils",
"version": "1.0",
"description": "TypeScript definitions for string-utils",
"license": "MIT",
"main": "",
"types": "index.d.ts",
"repository": {
"type": "git",
"url": "https://github.com/DefinitelyTyped/DefinitelyTyped.git",
"directory": "types/string-utils"
}
}
Test configuration (types/string-utils/tsconfig.json):
{
"compilerOptions": {
"module": "commonjs",
"lib": ["es6"],
"noImplicitAny": true,
"noImplicitThis": true,
"strictFunctionTypes": true,
"strictNullChecks": true,
"types": [],
"noEmit": true,
"strict": true
},
"files": ["index.d.ts"]
}
Validate with dtslint (DefinitelyTyped's official testing tool):
# Install
npm install -g dtslint
# Run tests from the types directory
dtslint types/string-utils
You can write type assertion comments inside the declaration file:
// index.d.ts
export declare function truncate(str: string, maxLength: number): string;
// $ExpectType string
truncate("hello world", 5);
// $ExpectError
truncate(123, 5); // wrong argument type — should error
typesVersions: Different Types for Different TypeScript Versions
When your library needs to support older TypeScript while providing more precise types for newer versions:
{
"name": "my-library",
"version": "2.0.0",
"types": "./dist/index.d.ts",
"typesVersions": {
">=4.7": {
"*": ["./dist/v4.7/*"]
},
">=4.0": {
"*": ["./dist/v4.0/*"]
}
}
}
TypeScript selects the type files matching the current compiler version.
@types/xxx vs Bundled Types
| Situation | Source | Notes |
|---|---|---|
package.json has a types field |
Library-bundled | No extra install needed |
Install @types/xxx |
DefinitelyTyped | Community-maintained, may lag behind |
| Both exist | Library-bundled wins | Can override with types tsconfig option |
TypeScript's resolution order: check package.json types/typings first, then @types, then index.d.ts in the directory.
Common Mistakes in Declaration Files
Mistake 1: Writing implementation code in a .d.ts file
// Wrong: .d.ts can only have types, not implementations
export declare function add(a: number, b: number): number {
return a + b; // SyntaxError: declaration files cannot have function bodies
}
// Correct
export declare function add(a: number, b: number): number;
Mistake 2: Missing the declare keyword
// Wrong: in a declaration file's global scope, missing declare
var globalVar: string; // treated as a value declaration — .d.ts can't have values
// Correct
declare var globalVar: string;
Mistake 3: Augmenting a module without importing it first
// Wrong: without import, this creates a new module instead of augmenting the existing one
declare module "express" {
interface Request {
user?: User;
}
}
// Correct: must import the original module first (even an empty import)
import "express";
declare module "express" {
interface Request {
user?: User;
}
}
Mistake 4: Mixing global and module declarations in one file
// If a file has import or export, it becomes a module file
// Global declarations in module files don't work
// Wrong (file has import, global declaration has no effect)
import { SomeType } from "./types";
declare var myGlobal: string; // not actually global
// Correct: wrap in declare global
import { SomeType } from "./types";
declare global {
var myGlobal: string;
}
Summary Table
| Syntax | Purpose | Example |
|---|---|---|
declare var/let/const |
Global variable | declare var __DEV__: boolean |
declare function |
Global function | declare function fetch(url: string): Promise<Response> |
declare class |
Global class | declare class EventEmitter { ... } |
declare namespace |
Global namespace | declare namespace MyApp { ... } |
declare module "..." |
Module types | declare module "lodash" { ... } |
declare module "*.png" |
Wildcard module | CSS Modules, image assets |
export = |
CommonJS default export | Corresponds to module.exports = xxx |
export default |
ESM default export | Corresponds to export default xxx |
declare global { } |
Global declaration in a module file | Augments global scope |
Next Chapter
Chapter 18 covers runtime validation: TypeScript types only exist at compile time, but API responses, user inputs, and environment variables are unknown types at runtime. The zod library lets you define a schema that simultaneously provides runtime validation logic and a TypeScript type, eliminating this fundamental inconsistency.