TL;DR
dts-bundle-generator is a standalone CLI tool that generates a single .d.ts bundle from your TypeScript library — no Rollup required, fast, handles re-exports and barrel files. rollup-plugin-dts is a Rollup plugin that bundles .d.ts files — integrates into your existing Rollup/Vite build pipeline, processes tsc output into a single declaration file. tsup dts is tsup's built-in declaration generation — tsup --dts generates declarations alongside your JavaScript bundle automatically. In 2026: tsup --dts for the simplest workflow, dts-bundle-generator for standalone CLI usage, rollup-plugin-dts for complex Rollup pipelines.
Key Takeaways
- dts-bundle-generator: ~500K weekly downloads — standalone CLI, no Rollup dependency
- rollup-plugin-dts: ~1M weekly downloads — Rollup plugin, processes tsc output
- tsup --dts: built into tsup (~5M weekly downloads) — zero config, esbuild + tsc
- The problem:
tscgenerates one.d.tsper source file — consumers want ONE file - A single
.d.tsbundle: smaller package, cleaner API surface, faster IDE autocomplete - All three produce the same result — a single declaration file for your package entry
The Problem
tsc output (individual files):
dist/
index.d.ts → re-exports from multiple files
utils/helpers.d.ts → internal types leaked
types/index.d.ts → intermediate types
core/parser.d.ts → implementation detail types
What npm consumers want:
dist/
index.d.ts → ONE file with all public types
index.js → bundled JavaScript
Benefits of bundled .d.ts:
✅ Smaller npm package (fewer files)
✅ Faster IDE autocomplete (one file to parse)
✅ Cleaner public API (internal types not exposed)
✅ No deep import accidents (users can't import internal types)
tsup --dts (Simplest)
tsup — built-in dts generation:
Zero config
# Generate JS bundle + .d.ts in one command:
tsup src/index.ts --dts --format esm,cjs
# Output:
# dist/index.js (ESM)
# dist/index.cjs (CJS)
# dist/index.d.ts (declarations)
# dist/index.d.cts (CJS declarations)
tsup.config.ts
import { defineConfig } from "tsup"
export default defineConfig({
entry: ["src/index.ts"],
format: ["esm", "cjs"],
dts: true, // Generate .d.ts
clean: true,
sourcemap: true,
splitting: false,
minify: false, // Don't minify library code
})
package.json exports
{
"name": "my-library",
"version": "1.0.0",
"type": "module",
"main": "./dist/index.cjs",
"module": "./dist/index.js",
"types": "./dist/index.d.ts",
"exports": {
".": {
"import": {
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
},
"require": {
"types": "./dist/index.d.cts",
"default": "./dist/index.cjs"
}
}
},
"files": ["dist"],
"scripts": {
"build": "tsup"
}
}
DTS-only mode
// tsup.config.ts — separate dts build for complex cases:
export default defineConfig([
{
entry: ["src/index.ts"],
format: ["esm", "cjs"],
// JS only:
},
{
entry: ["src/index.ts"],
dts: { only: true }, // DTS only — no JS output
// Use when dts generation needs different config
},
])
How tsup --dts works
1. tsup uses esbuild to bundle JavaScript (fast)
2. tsup runs tsc to generate individual .d.ts files
3. tsup uses rollup-plugin-dts internally to bundle into one .d.ts
4. Both outputs land in dist/
Note: tsup --dts is slower than JS-only builds because tsc runs
For faster dev builds: tsup (no --dts) in dev, tsup --dts for release
dts-bundle-generator
dts-bundle-generator — standalone CLI:
CLI usage
npx dts-bundle-generator -o dist/index.d.ts src/index.ts
# Options:
npx dts-bundle-generator \
--project tsconfig.json \
--out-file dist/index.d.ts \
--no-check \
src/index.ts
Programmatic API
import { generateDtsBundle } from "dts-bundle-generator"
const result = generateDtsBundle([
{
filePath: "./src/index.ts",
output: {
noBanner: true,
exportReferencedTypes: false, // Don't export internal types
},
},
])
// result[0] is the bundled .d.ts content as a string
Build script
{
"scripts": {
"build:js": "esbuild src/index.ts --bundle --outfile=dist/index.js --format=esm",
"build:dts": "dts-bundle-generator -o dist/index.d.ts src/index.ts",
"build": "npm run build:js && npm run build:dts"
}
}
Advantages
dts-bundle-generator vs tsc + rollup-plugin-dts:
✅ Single step — no intermediate .d.ts files
✅ No Rollup dependency
✅ Faster for simple libraries
✅ Standalone CLI — works with any bundler
Limitations:
❌ Slower than rollup-plugin-dts for complex projects
❌ Less ecosystem integration (not a Rollup/Vite plugin)
❌ Some edge cases with complex re-exports
rollup-plugin-dts
rollup-plugin-dts — Rollup integration:
Setup
npm install -D rollup-plugin-dts typescript
rollup.config.ts
import { defineConfig } from "rollup"
import dts from "rollup-plugin-dts"
export default defineConfig([
// JavaScript bundle:
{
input: "src/index.ts",
output: [
{ file: "dist/index.js", format: "esm" },
{ file: "dist/index.cjs", format: "cjs" },
],
plugins: [/* esbuild, typescript, etc. */],
},
// Declaration bundle:
{
input: "dist/types/index.d.ts", // tsc output
output: { file: "dist/index.d.ts", format: "es" },
plugins: [dts()],
},
])
Two-step workflow
{
"scripts": {
"prebuild": "tsc --emitDeclarationOnly --outDir dist/types",
"build": "rollup -c",
"postbuild": "rm -rf dist/types"
}
}
Step 1: tsc → generates dist/types/*.d.ts (individual files)
Step 2: rollup + rollup-plugin-dts → bundles into dist/index.d.ts
Step 3: cleanup → remove intermediate dist/types/
With Vite
// vite.config.ts — using rollup-plugin-dts in Vite library mode:
import { defineConfig } from "vite"
import dts from "rollup-plugin-dts"
export default defineConfig({
build: {
lib: {
entry: "src/index.ts",
formats: ["es", "cjs"],
},
rollupOptions: {
plugins: [dts()],
},
},
})
Feature Comparison
| Feature | tsup --dts | dts-bundle-generator | rollup-plugin-dts |
|---|---|---|---|
| Bundler | tsup (esbuild) | Standalone | Rollup/Vite |
| JS + DTS in one step | ✅ | ❌ (DTS only) | ❌ (DTS only) |
| Zero config | ✅ | CLI args | Rollup config |
| Requires tsc first | No (runs internally) | No | Yes |
| Complex re-exports | ✅ | ⚠️ | ✅ |
| Speed | Medium | Fast (simple) | Fast |
| ESM + CJS .d.ts | ✅ (.d.ts + .d.cts) | ❌ (single) | ❌ (single) |
| Weekly downloads | ~5M (tsup) | ~500K | ~1M |
When to Use Each
Use tsup --dts if:
- Want the simplest possible library build setup
- One command: JS + declarations + source maps
- ESM + CJS dual output with matching declaration files
- Don't want to manage Rollup config
Use dts-bundle-generator if:
- Using esbuild, swc, or a non-Rollup bundler for JavaScript
- Need standalone CLI without Rollup dependency
- Simple library with straightforward exports
- CI scripts where you want explicit control
Use rollup-plugin-dts if:
- Already have a Rollup/Vite build pipeline
- Complex library with multiple entry points
- Need fine-grained control over declaration bundling
- Integrating into an existing rollup.config.ts
Declaration Maps and Source Navigation for Library Consumers
A feature that significantly improves the consumer experience of published TypeScript libraries is declaration maps — .d.ts.map files that map from the bundled declaration file back to original TypeScript source files. When a consumer of your library command-clicks a function in VS Code, declaration maps allow the IDE to navigate to the actual TypeScript source file rather than stopping at the bundled .d.ts. This requires distributing your source files alongside the declaration bundle, which is feasible when your source is published on npm (either because the package includes src/ or because you publish source maps pointing to a public repository).
tsup supports declaration maps via dts: { resolve: true } in the config, and the sourcemap: true option generates source maps for the JavaScript bundle simultaneously. The combination of declaration maps and source maps gives consumers both type-safe navigation and meaningful stack traces that point into your original TypeScript source. This is particularly valuable for libraries with complex generic types where consumers frequently need to inspect the source to understand type constraints.
rollup-plugin-dts generates declaration maps when sourcemap: true is passed to the Rollup output config for the declarations build configuration. dts-bundle-generator also supports declaration map generation via the --out-file-declaration-map flag. For all three tools, enabling declaration maps adds minimal build time but meaningfully improves the debugging experience for library consumers working with complex types.
Managing Public API Surface with TypeScript Declaration Files
The .d.ts bundle is your library's public contract with TypeScript consumers. Beyond generating the file, the content of your declaration file determines what types consumers have access to and shapes their experience with your library in IDEs. Several TypeScript configuration decisions affect what ends up in the bundled declaration file.
The stripInternal compiler option in tsconfig.json removes any declaration annotated with /** @internal */ from the emitted .d.ts files. Combined with API Extractor's @internal tag (which serves the same purpose for publishing workflows), this gives you a way to expose types within your package's own source for cross-module use while keeping them out of the public API. This is the recommended pattern for large libraries with multiple entry points that need to share types internally without making those types part of the public contract.
Conditional exports in package.json interact with declaration files in ways that require careful configuration. A library with "exports": { ".": "./dist/index.js", "./utils": "./dist/utils.js" } needs matching declaration files: "./utils": { "types": "./dist/utils.d.ts" }. tsup handles dual-entry builds via the entry array (entry: ["src/index.ts", "src/utils.ts"]) and generates matching declaration files automatically. dts-bundle-generator processes one entry point at a time, so multi-entry libraries require running it once per entry and combining the scripts. rollup-plugin-dts supports multiple entry points through Rollup's multi-input configuration.
Community Adoption in 2026
Weekly npm downloads (early 2026 estimates):
| Package | Weekly Downloads | GitHub Stars | Maintained By |
|---|---|---|---|
| tsup | ~5M | 9,000+ | egoist |
| rollup-plugin-dts | ~1M | 1,100+ | Community |
| dts-bundle-generator | ~500K | 1,700+ | nicolo-ribaudo |
tsup's download numbers are high because it is a complete build solution — many projects that just want tsup --dts do not care about the underlying dts mechanism. The choice is often tsup vs "roll your own Rollup config" rather than tsup vs dts-bundle-generator.
rollup-plugin-dts was the default dts bundling approach before tsup made it the default. Many projects still use it explicitly in their rollup.config.ts because they were set up before tsup popularized the all-in-one approach. It has very good stability — the API hasn't changed meaningfully in years.
dts-bundle-generator has fewer downloads but higher stars relative to its download count — it is a niche tool that people who know about it tend to love. The approach of "one CLI command, one output file" appeals to developers who want minimal tooling and explicit control.
Build Speed Comparison
DTS generation is the slowest part of TypeScript library builds because it requires the TypeScript compiler to run full type checking:
| Tool | Build time (small library, ~20 files) | Notes |
|---|---|---|
| tsup --dts | 8-15 seconds | esbuild for JS, tsc + rollup-plugin-dts for DTS |
| dts-bundle-generator | 6-12 seconds | Uses TypeScript compiler API directly |
| rollup-plugin-dts | 5-10 seconds | Processes tsc output (tsc runs separately) |
| tsup (no --dts) | 0.5-2 seconds | esbuild only — very fast |
The times are similar because all three eventually invoke the TypeScript compiler. tsup without --dts is dramatically faster — this is worth knowing for development builds where you only need --dts on release builds.
For CI where build time matters, a common pattern is:
{
"scripts": {
"dev": "tsup --watch",
"build": "tsup --dts",
"build:js-only": "tsup"
}
}
Use build:js-only in CI if TypeScript types are pre-validated by a separate tsc --noEmit step.
Real-World Adoption: Who Uses What
tsup --dts is the standard choice for new TypeScript libraries in 2026. The UnJS ecosystem (Nitro, h3, ofetch, defu) uses tsup or unbuild (which uses rollup-plugin-dts internally). Vitest, Radix UI, and most modern TypeScript packages on npm use tsup or a similar zero-config build tool with built-in dts generation.
rollup-plugin-dts is used by projects that predate tsup's dominance or that have complex multi-entry builds that need fine-grained control. Vue's own packages use Rollup directly with rollup-plugin-dts. Projects like vite-plugin-* that ship alongside Vite use rollup-plugin-dts in their build pipelines.
dts-bundle-generator is the choice when you want the absolute minimum toolchain. If your JavaScript is bundled by esbuild directly (without Rollup), dts-bundle-generator lets you add declaration generation without pulling in the entire Rollup ecosystem. It is popular in CLI tools and packages with simple public APIs.
Migration Guide
Adding dts to an existing tsup build:
// tsup.config.ts — before (no types):
import { defineConfig } from "tsup"
export default defineConfig({
entry: ["src/index.ts"],
format: ["esm", "cjs"],
})
// After (add dts: true):
import { defineConfig } from "tsup"
export default defineConfig({
entry: ["src/index.ts"],
format: ["esm", "cjs"],
dts: true, // That's it
})
Migrating from rollup-plugin-dts to tsup --dts:
# Remove rollup dependencies:
npm uninstall rollup rollup-plugin-dts
# Install tsup:
npm install -D tsup
# Update package.json scripts:
# Before: "build": "tsc --emitDeclarationOnly && rollup -c rollup.dts.config.ts"
# After: "build": "tsup --dts"
From dts-bundle-generator to tsup --dts:
# Before:
# package.json: "build:dts": "dts-bundle-generator -o dist/index.d.ts src/index.ts"
# After:
# package.json: "build": "tsup --dts"
# tsup handles both JS and dts in one command
The migration from any standalone dts tool to tsup is straightforward — tsup's dts: true config option replaces whatever dts pipeline you had with a single setting.
Common Pitfalls and Troubleshooting
"Could not find a declaration file for module"
This error means consumers of your library cannot find your type declarations. The most common cause is a misconfigured types field in package.json or a missing --dts flag in your build command.
// package.json — correct configuration:
{
"main": "./dist/index.cjs",
"module": "./dist/index.js",
"types": "./dist/index.d.ts", // Must point to real file
"exports": {
".": {
"import": {
"types": "./dist/index.d.ts", // Required for Node 16+ resolution
"default": "./dist/index.js"
},
"require": {
"types": "./dist/index.d.cts",
"default": "./dist/index.cjs"
}
}
}
}
Always run ls dist/ after building to verify the .d.ts file exists before publishing.
Re-export issues with dts-bundle-generator
If your library re-exports from node_modules (e.g., export type { Options } from "some-dep"), dts-bundle-generator may fail to resolve those types. The fix:
// tsup handles this automatically — use tsup for libraries with re-exported third-party types
// For dts-bundle-generator, use the inlinedLibraries option:
const result = generateDtsBundle([{
filePath: "./src/index.ts",
libraries: {
inlinedLibraries: ["some-dep"], // Inline types from this dep
},
}])
tsconfig paths not resolving
All three tools respect tsconfig.json paths aliases, but require the paths to be resolvable at build time. If you use @/utils path aliases, ensure your tsconfig paths are configured and the tool can find them.
// tsconfig.json — path aliases:
{
"compilerOptions": {
"paths": {
"@/*": ["./src/*"]
}
}
}
For tsup, path aliases are resolved automatically via esbuild. For rollup-plugin-dts, add @rollup/plugin-alias. For dts-bundle-generator, path aliases are resolved via the TypeScript compiler — just ensure the correct tsconfig is passed via --project.
Methodology
Download data from npm registry (weekly average, February 2026). Feature comparison based on tsup v8.x, dts-bundle-generator v9.x, and rollup-plugin-dts v6.x.
Compare TypeScript build tools and bundler packages on PkgPulse →
See also: Rollup vs webpack and Bun vs Vite, archiver vs adm-zip vs JSZip (2026).