Featured image of post Exporting Proper TypeScript Declaration Files (.d.ts) for NPM Featured image of post Exporting Proper TypeScript Declaration Files (.d.ts) for NPM

Exporting Proper TypeScript Declaration Files (.d.ts) for NPM

Design libraries distributed on npm to load typings natively via dual ESM/CJS exports structures.

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"
  }
}
OptionPurpose
declarationEmits .d.ts files alongside .js output
declarationMapGenerates source maps for declarations (go-to-definition in editors)
emitDeclarationOnlyOnly emits declarations, not JS (use when another tool handles bundling)
outDirOutput 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

IssueSolution
Missing .d.ts for .js filesSet declaration: true in tsconfig
Consumers see any instead of proper typesVerify types field points to the correct entry
Dual ESM/CJS types not resolvingUse exports with separate types conditions
Declaration source maps brokenAdd declarationMap: true
Private types leaked in public APIPrefix 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.