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.
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
- Write JS + handcrafted
.d.ts— for JS-only libraries or shims. - 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 fields —
name?: stringvsname: 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 →