Why Declaration Files Matter
When you publish an npm package written in TypeScript, consumers need type information to get IntelliSense and compile-time checking. Without .d.ts files, your library is effectively typed as any, defeating the purpose of using TypeScript in the first place.
Declaration files describe the shape of your exports without shipping the implementation source. They enable tree-shaking, documentation hover tips, and strict type checking in consumer projects.
Generating .d.ts Files
The TypeScript compiler generates declaration files when declaration is set to true in tsconfig.json.
{
"compilerOptions": {
"declaration": true,
"declarationMap": true,
"emitDeclarationOnly": true,
"outDir": "./dist",
"rootDir": "./src"
}
}
| Option | Purpose |
|---|---|
declaration | Emits .d.ts files alongside .js output |
declarationMap | Generates source maps for declarations (go-to-definition in editors) |
emitDeclarationOnly | Only emits declarations, not JS (use when another tool handles bundling) |
outDir | Output directory for both JS and .d.ts files |
Run the build:
npx tsc
The output structure mirrors your src/ layout. A file src/index.ts produces dist/index.js and dist/index.d.ts.
Package.json Types Field
The types field (or its alias typings) points to the entry declaration file.
{
"name": "my-library",
"main": "./dist/index.js",
"types": "./dist/index.d.ts"
}
For dual ESM/CJS packages, the exports field provides conditional resolution:
{
"exports": {
".": {
"import": {
"types": "./dist/index.d.mts",
"default": "./dist/index.mjs"
},
"require": {
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
}
},
"./subpath": {
"import": {
"types": "./dist/subpath.d.mts",
"default": "./dist/subpath.mjs"
},
"require": {
"types": "./dist/subpath.d.ts",
"default": "./dist/subpath.js"
}
}
}
}
This tells TypeScript to load index.d.mts for import statements and index.d.ts for require() calls.
Triple-Slash Directives
In hand-written ambient declarations, use triple-slash directives to reference other types.
/// <reference types="node" />
/// <reference path="./custom-types.d.ts" />
export function readConfig(path: string): Buffer;
These are only valid in .d.ts files, never in implementation .ts files. For generated declarations, rely on normal import statements instead.
Including Declarations in Your Package
The files field in package.json controls what gets published to npm:
{
"files": [
"dist",
"!dist/**/*.test.d.ts",
"README.md",
"LICENSE"
]
}
Always exclude test declaration files and source .ts files unless you intend to ship them.
Testing Your Declarations
Before publishing, validate that consumers can resolve your types correctly.
# Create a test project
mkdir test-consumer && cd test-consumer
npm init -y
npm link ../my-library
import { MyType } from "my-library";
const x: MyType = { /* ... */ };
Run npx tsc --noEmit in the test project. If it compiles without errors, your declarations are correct.
Common Pitfalls
| Issue | Solution |
|---|---|
Missing .d.ts for .js files | Set declaration: true in tsconfig |
Consumers see any instead of proper types | Verify types field points to the correct entry |
| Dual ESM/CJS types not resolving | Use exports with separate types conditions |
| Declaration source maps broken | Add declarationMap: true |
| Private types leaked in public API | Prefix internal types with _ or use @internal JSDoc tag |
Summary
Generating and publishing declaration files correctly ensures your TypeScript library provides a first-class developer experience. Configure declaration: true in tsconfig, set the types field in package.json, use exports for dual-module packages, and always test your declarations against a consumer project before publishing.
