TypeScript Declaration Files
Parent: TypeScript Expert · researched 2026-06-03T23:22:47.968Z· 10 sources · 7 concepts · skill typescript-declaration-files
A lang-js-ts hub reference for producing and shipping TypeScript type information: how .d.ts
TypeScript Declaration Files (.d.ts Authoring & Type Distribution)
- A lang-js-ts hub reference for producing and shipping TypeScript type information: how .d.ts [source]
- files are emitted, how to hand-author ambient declarations and augment other people's types, and how [source]
- to distribute types from an npm package so downstream consumers (and tsc, attw, editors) resolve [source]
- them correctly across ESM and CJS. [source]
- A .d.ts is types only - no runtime code, no JS output. It is the contract tsc reads when it [source]
- can't see a library's source. Scope discipline: declaration emit compiler options live here (not in [source]
- typescript-compiler-config); the runtime resolution algorithm that finds the file lives in [source]
- nodejs-module-resolution; the bundler that produces the file lives in [source]
- nodejs-build-tooling-bundlers; advanced type operators used inside a .d.ts live in [source]
- typescript-advanced-types. [source]
Overview
- There are two ways a .d.ts comes into existence: emitted by the compiler from .ts source [source]
- (declaration: true), or hand-authored as an ambient declaration for code TypeScript can't analyze [source]
- (plain JS libs, globals injected by a <script>, env vars). Distribution is the third axis: a package [source]
- points consumers at its types via the types field and the exports map, or - if it ships no types — [source]
- the community publishes them under @types/* via DefinitelyTyped. [source]
- Version anchors (memorize - these drive "does my TS have X" questions): [source]
1. Declaration emit — turning `.ts` into `.d.ts`
- With declaration: true, tsc emits a .d.ts next to each .js it produces. The four knobs: [source]
- Why explicit return types help emit (this is load-bearing). When a function lacks an annotated [source]
- return type, the emitter must infer the type and write it into the .d.ts. Inference can be slow on [source]
- complex code, and worse, it can produce a declaration that **references a symbol the consumer can't [source]
- name** - TS then errors with TS2742 ("inferred type cannot be named without a reference to …") or [source]
- TS4082 ("default export of the module has or is using private name …"). Annotating the public surface [source]
- (exported function/method return types, exported const types) makes emit a near-mechanical copy and [source]
- sidesteps the un-nameable-symbol class of failures entirely. This is exactly what isolated declarations [source]
2. `--isolatedDeclarations` (TS 5.5)
- isolatedDeclarations: true forces the public API to be **explicitly typed enough that a .d.ts can be [source]
- generated from a single file without consulting any other file**. That one-in/one-out property lets [source]
- non-tsc tools (oxc, swc, esbuild's experimental path) emit declarations in parallel, per file, [source]
- which tsc's whole-program declaration emit can't do. [source]
- Requires declaration: true (or composite: true) - it's a stricter mode of declaration emit, [source]
- so the compiler errors if neither is set. (isolatedModules is a recommended companion for the same [source]
- "per-file" philosophy, but it is not the enforced prerequisite - declaration/composite is.) [source]
- **What it enforces (representative error: TS9007, "Function must have an explicit return type [source]
- annotation with --isolatedDeclarations"; the TS900x/TS903x family covers the other [source]
- inference-blocking cases):** [source]
- Every exported function / arrow / function-expression assigned to an exported binding needs an [source]
- explicit return type. [source]
- Exported let/const/var need an annotation or a trivially-inferable literal initializer. [source]
- Public and protected class members (fields, accessors, method return types) need annotations. [source]
- Constructs whose emitted type can't be computed locally are rejected (e.g. spreading a value whose [source]
- type comes from another file, computed property keys whose type needs inference). [source]
3. Hand-authoring `.d.ts` — the global-vs-module rule
- The single most important authoring fact: **a .d.ts with any top-level import or export is a [source]
- module - its declarations are scoped, not global. A .d.ts with no** top-level import/export [source]
- is a script: every declared name lands in the global scope. moduleDetection is always auto [source]
- for .d.ts, so you cannot force this with config - it's purely structural. [source]
- To put global types in a file that is a module (has imports/exports), wrap them in declare global: [source]
- Triple-slash directives (must be at the very top, before any statement): [source]
- In published declaration files, use /// <reference types="..." /> to declare a dependency on [source]
- another package's types; do not use /// <reference path="..." /> (the TS team explicitly flags it [source]
- as a red flag - it bakes in a relative file layout). Prefer real imports where the file is a module. [source]
4. Declaration merging & module augmentation
- TypeScript merges multiple declarations of the same name in the same scope. This is the mechanism [source]
- behind extending types you don't own. [source]
- Interface merging - same-named interfaces combine their members: [source]
- Namespace + function/class/enum merging - a namespace merges with a same-named function, class, or [source]
- enum, letting you hang static-like members off a callable/constructable: [source]
- Module augmentation - re-open another package's module from a file that is itself a module and [source]
- declare module "their-pkg". The augmenting declarations merge into the original; you cannot add [source]
- new top-level exports this way, only augment existing shapes. [source]
- Augmenting globals from inside a module uses declare global. The two canonical Node patterns: [source]
5. Shipping types from a package
- Bundled (most common): emit .d.ts with your build and point at the entry declaration. [source]
- With an exports map (modern, and node16/nodenext resolution requires it): add a types [source]
- condition. It MUST appear first in each condition block - TypeScript reads conditions top-to-bottom [source]
- and stops at the first match, so a types placed after import/require/default is silently [source]
- Dual-package types (the gotcha): when you ship both ESM and CJS, a single index.d.ts is wrong if [source]
- your package is "type": "module" - under require, the consumer's compiler sees the .d.ts as ESM [source]
- syntax describing a CJS file. Ship two declaration files: .d.mts for the import condition and [source]
- .d.cts for the require condition (or index.d.ts + index.d.cts). This is precisely what [source]
- @arethetypeswrong/cli flags as "Masquerading as CJS/ESM." [source]
- typesVersions - serve different declarations to older TypeScript versions, or remap subpaths: [source]
- UMD global (export as namespace) - for a library usable both as a module and as a <script> global: [source]
- export as namespace id is only legal in a .d.ts that also has other top-level exports; placing it [source]
- in a .ts errors with TS1315. It only takes effect when the file enters compilation via a [source]
- triple-slash reference or as a top-level input - it's a no-op when the package is imported. [source]
Tools / Frameworks
- tsc - tsc --emitDeclarationOnly (types-only build), tsc -b (project references; needs [source]
- composite: true, which implies declaration: true). [source]
- @arethetypeswrong/cli (attw) - attw --pack . or attw <tarball> validates that published [source]
- types resolve under every module/condition combination. Flags "Masquerading as CJS/ESM," [source]
- "Fallback Condition," "No types," "missing package.json exports." Run in CI before publish. [source]
- tsd - type-level test runner (uses expect-type under the hood). Write *.test-d.ts files with [source]
- expectType<T>(value) and expectError(...); tsd runs the compiler and asserts. Configure via a [source]
- tsd block in package.json. [source]
- Vitest type testing - alternative to tsd: *.test-d.ts with expectTypeOf(x).toEqualTypeOf<T>() [source]
- or the simpler assertType<T>(x); gated behind typecheck config (statically analyzed, never run). [source]
- **@types/* + DefinitelyTyped (DT)** - the community type registry, auto-published to the @types [source]
- npm org from the DT monorepo. [source]
Methodology
- Emit vs. hand-author. If you own the .ts source, prefer emitted .d.ts (`declaration: [source]
- true`) - never hand-maintain types that duplicate your source. Hand-author only for untyped JS deps, [source]
- ambient globals, or non-code imports. [source]
- Annotate the public surface (exported return types, exported const types). Consider turning on [source]
- isolatedDeclarations: true to enforce it and unlock parallel/third-party d.ts emit. [source]
- Decide global vs. module per file by the top-level import/export rule; reach for declare global [source]
- only inside module files; reach for declare module "x" to augment a dependency. [source]
- Wire distribution: types field for the simple case; an exports map with a leading types [source]
- condition for node16/nodenext; split .d.mts/.d.cts for dual packages. [source]
- Validate before publish: attw --pack . for resolution correctness, tsd/Vitest for the type [source]
Consuming & contributing `@types` / DefinitelyTyped
- Consuming. tsc auto-includes every @types/* package found under node_modules/@types (and parent [source]
- node_modules). Two tsconfig knobs scope this: [source]
- typeRoots - which folders hold ambient type packages (default ["./node_modules/@types"]). [source]
- types - an allow-list; "types": ["node", "jest"] includes only those, excluding all other [source]
- @types/* from the global scope. Use it to stop unrelated global types (e.g. a stray @types/mocha) [source]
- from polluting a project. (Package-scoped, imported types are unaffected - this gates only the [source]
- automatically-included ambient packages.) [source]
- Contributing a NEW types package to DefinitelyTyped. Create types/foo/ with these files (the old [source]
- triple-slash header comment is gone - metadata now lives in a package.json): [source]
- Tests use dtslint assertions inside foo-tests.ts: // $ExpectType string on the line above an [source]
- expression, and // @ts-expect-error for code that must fail to compile. Validate locally with [source]
- pnpm test (DT runs dtslint + attw); after merge, the @types/foo package publishes automatically [source]
- within a few hours. Declare runtime-type dependencies in dependencies (not devDependencies) so [source]
- consumers pull them transitively. [source]
Anti-Patterns
- Putting the types condition after import/require/default in exports - TS stops at the [source]
- first match and never sees it. types goes first. [source]
- Shipping one index.d.ts for a dual ESM+CJS "type": "module" package - "Masquerading as CJS." Ship [source]
- Hand-maintaining .d.ts that mirror your own .ts source instead of emitting them - they drift. [source]
- /// <reference path="..." /> in published types - bakes in a file layout; use [source]
- /// <reference types="..." /> or real imports. [source]
- Exporting un-annotated functions from a library entry and being surprised by TS2742/TS4082 [source]
- ("cannot be named") on emit - annotate the public surface. [source]
- Expecting declare module "x" (module augmentation) to add new top-level exports - it can only [source]
- augment existing shapes; the augmenting file must itself be a module. [source]
- Using let/const to declare a global variable in a declare global block - use var. [source]
- Relying on tsconfig paths to be honored in published types - consumers don't share your paths; [source]
- emit fully-resolved specifiers (the Node-native runner ignores paths too, per the sibling [source]
- native-strip-types reference). [source]
Troubleshooting
- error TS9007 (or TS900x) under isolatedDeclarations → add the explicit return type / [source]
- annotation it points at. [source]
- isolatedDeclarations can only be used when … declaration … is enabled → set declaration: true [source]
- or composite: true. [source]
- TS2742 / TS4082 "cannot be named" on emit → annotate the export, or export the referenced [source]
- symbol so it's nameable. [source]
- Consumer "Could not find a declaration file for module 'foo'" → ship a types field / types [source]
- condition, or npm i -D @types/foo, or write a local declare module "foo" stub. [source]
- attw "Masquerading as CJS/ESM" / "Fallback Condition" → fix the exports conditions; provide the [source]
- matching .d.mts/.d.cts for each runtime entry. [source]
- declare global "Augmentations for the global scope can only be nested in … modules" → the file [source]
- isn't a module; add export {};. [source]
- TS1315 "Global module exports may only appear in declaration files" → export as namespace is in [source]
- a .ts; move it to a .d.ts. [source]
- Global types from a .d.ts don't appear → the file has a top-level import/export, so it's a [source]
- module; either remove them or wrap the globals in declare global. [source]
References
- TS Handbook - Declaration Files (Introduction / Library Structures / Templates: module, global, [source]
- global-plugin, module-plugin): https://www.typescriptlang.org/docs/handbook/declaration-files/introduction.html [source]
- TS Handbook - Declaration Merging: https://www.typescriptlang.org/docs/handbook/declaration-merging.html [source]
- TS Handbook - Modules (module vs. script rule): https://www.typescriptlang.org/docs/handbook/2/modules.html [source]
- TS Handbook - Publishing: https://www.typescriptlang.org/docs/handbook/declaration-files/publishing.html [source]
- TSConfig - isolatedDeclarations / declaration / declarationMap / emitDeclarationOnly / declarationDir: [source]
- https://www.typescriptlang.org/tsconfig/#isolatedDeclarations [source]
- TS 5.5 release notes (isolatedDeclarations): https://www.typescriptlang.org/docs/handbook/release-notes/typescript-5-5.html [source]
- microsoft/TypeScript #58944 - Isolated Declarations: state of the feature: https://github.com/microsoft/TypeScript/issues/58944 [source]
- DefinitelyTyped README + contribution guide: https://github.com/DefinitelyTyped/DefinitelyTyped / https://definitelytyped.org/guides/contributing.html [source]
- @arethetypeswrong/cli (problem kinds: FalseCJS/FalseESM/FallbackCondition): https://github.com/arethetypeswrong/arethetypeswrong.github.io [source]
- tsd: https://github.com/tsdjs/tsd ; Vitest "Testing Types": https://vitest.dev/guide/testing-types [source]
- microsoft/TypeScript #26532 - export as namespace (UMD): https://github.com/microsoft/TypeScript/issues/26532 [source]
Children
- No children recorded.