Authoring `.d.ts`

Publishing a TS Library — Types Done Right

Authoring `.d.ts`

Whether you're shimming an untyped dep or publishing your own library, here are the patterns that make `.d.ts` files pleasant.

4 min read Level 3/5 #typescript#declarations#dts
What you'll learn
  • Author a clean `.d.ts` for a small library
  • Emit declarations from a TS source library
  • Avoid the most common publication pitfalls

For library authors, .d.ts is the public face of your package. Get it right and consumers love you. Get it wrong and they file issues.

Two Paths

  1. Write JS + handcrafted .d.ts — for JS-only libraries or shims.
  2. Write TS, emit .d.ts — by far the more common path now.

Path 2 — Emit From TS Source

{
  "compilerOptions": {
    "declaration": true,
    "declarationMap": true,
    "outDir": "dist"
  },
  "include": ["src"]
}

tsc produces dist/index.js plus dist/index.d.ts — and declaration maps so consumers can “Go to Definition” into your source.

Your package.json:

{
  "main": "dist/index.js",
  "types": "dist/index.d.ts",
  "exports": {
    ".": {
      "types": "./dist/index.d.ts",
      "default": "./dist/index.js"
    }
  }
}

The types condition must come first in the conditional exports, otherwise TS doesn’t find it.

Path 1 — Handcrafted .d.ts

For wrapping a JS-only library, write a .d.ts next to it:

// some-lib.d.ts
export interface Options { /* ... */ }
export function doThing(input: string, options?: Options): number;
export default function createInstance(): { /* ... */ };

Match the runtime’s actual exports exactly. If the library uses module.exports = function() {}, you need:

declare function createInstance(): { /* ... */ };
export = createInstance;

The export = form is for CommonJS-style “module as a single value”.

Common Pitfalls

  • Importing internal modules in your .d.ts — consumers see those imports too. Inline or re-export only what’s part of your public API.
  • Forgetting to mark optional fieldsname?: string vs name: string. Be precise.
  • Re-exporting any — leaking the lack of types from a sub-dep onto your consumers.
  • bivarianceHack — old type definitions used a method-property syntax trick to relax variance. Don’t write new bivariance hacks; the strict default is correct.

Versioning Types

Treat type changes the same as runtime changes. Adding a required field is a breaking change. Tightening a type from string | number to string is a breaking change. Semver applies.

Up Next

Using TS with React — the parts unique to JSX.

TS in React →