Fixing tsup Output Missing .d.ts Type Declarations
Your tsup build produces .mjs and .cjs files but no .d.ts, so consumers get a 'could not find a declaration file' error. This page covers the exact symptom, why declaration emit is skipped, and the configuration that restores it.
Exact symptoms and error messages
The JavaScript builds, but the dist folder has no declaration files, and consumers hit:
Could not find a declaration file for module 'your-lib'.
'/node_modules/your-lib/dist/index.mjs' implicitly has an 'any' type.
ls dist shows index.mjs and index.cjs but no index.d.ts or index.d.cts.
Root cause analysis
tsup delegates declaration generation to a separate step; if dts is not enabled, or if tsc cannot type-check your entry (a type error, a missing tsconfig, or skipLibCheck interactions), tsup emits JavaScript and silently omits the declarations. Because the runtime files exist, the build looks successful — the failure only surfaces in a consumer, exactly the class of problem described in TypeScript Declaration Publishing.
There are three distinct ways declarations go missing, and they need different fixes. The first is simply not enabling dts at all. The second is a type error in the entry graph: tsup's declaration pass runs a real type-check, and when it fails it logs the error but still emits the JavaScript, so a green-looking build ships without types. The third is a monorepo path issue — an unresolved paths alias or a project reference the declaration pass cannot follow — which produces partial or empty declarations. All three surface only in a consumer, the late-failure pattern that TypeScript Declaration Publishing exists to prevent.
Declaration bundling adds a fourth wrinkle. Even when .d.ts files emit, if they reference an internal type from a deep path that the files allowlist excludes, the consumer's checker follows a dangling import and reports the type as any. That is why verifying with a resolver that mimics the consumer — @arethetypeswrong/cli — catches problems a local tsc against your own source never will.
Resolution and configuration patch
Enable declaration emit and make sure the entry type-checks:
// tsup.config.ts
import { defineConfig } from 'tsup';
export default defineConfig({
entry: ['src/index.ts'],
format: ['esm', 'cjs'],
dts: true,
clean: true,
});
Then wire both declaration flavors through exports so each condition resolves its own types, as covered in Generating Dual CJS/ESM Type Definitions.
For large libraries where the extra type-check makes tsup slow, split declaration emit into a dedicated, cacheable step and let tsup handle only JavaScript:
// package.json
{
"scripts": {
"build:js": "tsup src/index.ts --format esm,cjs",
"build:types": "tsc --emitDeclarationOnly --declaration --outDir dist",
"build": "npm run build:js && npm run build:types"
}
}
This keeps the transpile fast and makes the declaration step independently cacheable in CI, while still producing the .d.ts files the exports map points at.
CLI validation and debug commands
# Confirm declarations now emit
pnpm build && ls dist/*.d.*
# Prove the types resolve the way a consumer's tooling will
pnpm exec attw --pack .
# Catch a type error that would silently skip dts
pnpm exec tsc --noEmit
Prevention and CI guardrails
- Run
tsc --noEmitbefore the bundler so a type error fails loudly instead of dropping declarations. - Add
@arethetypeswrong/clito CI to catch a missing or mis-wired declaration. - Assert
dist/index.d.tsexists in a post-build check. - Keep
dts: true(or a dedicatedtsc --emitDeclarationOnlystep) in the build script, not just locally.
Wiring declarations through conditional exports
Emitting the files is only half the job; the exports map has to point each condition at the matching declaration, or a consumer under node16/nodenext resolution still gets any. Under those modes the type checker follows the same conditional resolution as the runtime, so the import branch needs a .d.ts and the require branch needs a .d.cts.
{
"exports": {
".": {
"import": { "types": "./dist/index.d.ts", "default": "./dist/index.mjs" },
"require": { "types": "./dist/index.d.cts", "default": "./dist/index.cjs" }
}
}
}
With the nested types conditions in place, tsc resolves the format-appropriate declaration for each import style, and attw reports a clean matrix instead of a false CJS or no types cell.
Making the type contract a CI gate
The reason a missing declaration reaches a consumer is that the local build looks green. Close that gap by resolving your own package the way a consumer will, inside CI. @arethetypeswrong/cli packs the tarball and checks every module/resolution combination, so a dropped or mis-wired declaration fails your pipeline instead of a user's editor.
- run: pnpm build
- run: pnpm exec attw --pack .
- run: test -f dist/index.d.ts && test -f dist/index.d.cts
The explicit test -f assertions are a cheap backstop: even if a future config change silently disables dts, the missing file fails the job. Together they turn 'the types are wrong' from a report you get weeks later into a red build on the PR that caused it.
Frequently Asked Questions
Does dts: true slow the build down?
Yes, declaration generation adds a type-checking pass, which is slower than esbuild's transpile-only path. For large libraries, a separate cached tsc --emitDeclarationOnly step can be faster in CI.
Why do I need both .d.ts and .d.cts?
Under node16/nodenext resolution, the type checker follows the same conditional exports as the runtime, so the require branch needs a CommonJS-flavored declaration next to its .cjs file.
Why does the JavaScript build succeed but declarations silently vanish?
Because tsup's declaration pass is a separate type-check. When it hits a type error it logs it but still emits the transpiled JS, so the build exits green with no .d.ts. Run tsc --noEmit first so the type error fails the build loudly.
Can I emit one declaration file for both ESM and CJS?
Only under bundler/node10 resolution. Under node16/nodenext the checker follows conditional exports, so the require branch needs its own .d.cts. Emitting both is the safe default for a dual-published package.
Related
- Generating Dual CJS/ESM Type Definitions — the dual-declaration recipe this fix depends on.
- Bundling and Build Tooling for Libraries — the overview for library build configuration.