Best TypeScript-First Build Tools 2026
esbuild is 45x faster than tsc for transpilation. SWC is 20x faster. Yet tsc remains the gold standard for type checking — because esbuild and SWC deliberately skip type checking entirely. The TypeScript build tooling landscape in 2026 is about combining these tools correctly, not picking one.
TL;DR
For libraries: Use tsup (esbuild-based, zero-config) or unbuild (Rollup-based, better tree-shaking). For applications: Your framework's build tool (Next.js, Vite, etc.) already handles this. For type checking: Always run tsc --noEmit separately — no other tool can replace it. For running TypeScript directly: Use tsx (fast, esbuild-based).
Key Takeaways
tsc: Only tool that performs type checking; transpilation is 45x slower than esbuildesbuild: 45x faster than tsc for transpilation; does NOT type checkswc: 20x faster than tsc; does NOT type check; used by Rspack/Next.js internalstsup(~1.2M weekly downloads): Most popular library bundler, uses esbuild, zero-configunbuild: Rollup-based, better for libraries needing optimal tree-shakingpkgroll: Rollup-based, explicit about entry points, growing adoption- The correct pattern:
tsc --noEmit(type check) + esbuild/SWC (transpile)
The TypeScript Tooling Landscape
The tools serve different purposes:
Source (TypeScript)
│
├─→ Type Checking: tsc --noEmit (required for correctness)
│
├─→ Transpilation (TS → JS):
│ tsc (slow, correct)
│ esbuild (45x faster, no type check)
│ SWC (20x faster, no type check)
│ Babel (configurable, slow)
│
└─→ Bundling (JS → optimized JS):
tsup (esbuild, library-focused)
unbuild (Rollup, library-focused)
pkgroll (Rollup, library-focused)
Vite (Rollup + esbuild)
Rollup (manual configuration)
tsc: The Type Checking Foundation
Package: typescript (includes tsc)
Weekly downloads: 60M+
Purpose: Type checking + transpilation (slow path)
Never use tsc for production builds in 2026. Use it only for type checking:
# Type check only (no output files)
npx tsc --noEmit
# Or in watch mode during development
npx tsc --noEmit --watch
// package.json
{
"scripts": {
"type-check": "tsc --noEmit",
"build": "tsup src/index.ts --format esm,cjs --dts"
}
}
tsconfig.json for Library Publishing
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "bundler",
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"strict": true,
"skipLibCheck": true,
"noUncheckedIndexedAccess": true,
"exactOptionalPropertyTypes": true
},
"include": ["src"],
"exclude": ["node_modules", "dist"]
}
tsup: The Library Bundler Standard
Package: tsup
Weekly downloads: 1.2M
GitHub stars: 10K
Creator: EGOIST
Underlying: esbuild
tsup is the most popular TypeScript library bundler. Zero-config defaults make it productive immediately:
npm install -D tsup
Basic Library Setup
// tsup.config.ts
import { defineConfig } from 'tsup';
export default defineConfig({
entry: ['src/index.ts'],
format: ['cjs', 'esm'], // CommonJS + ES Modules
dts: true, // Generate .d.ts files
splitting: false, // Keep output in single files
sourcemap: true,
clean: true, // Clean dist/ before build
});
// package.json
{
"name": "my-library",
"version": "1.0.0",
"main": "./dist/index.cjs",
"module": "./dist/index.js",
"types": "./dist/index.d.ts",
"exports": {
".": {
"import": "./dist/index.js",
"require": "./dist/index.cjs",
"types": "./dist/index.d.ts"
}
},
"scripts": {
"build": "tsup",
"dev": "tsup --watch",
"type-check": "tsc --noEmit"
},
"devDependencies": {
"tsup": "^8.0.0",
"typescript": "^5.4.0"
}
}
Multiple Entry Points
export default defineConfig({
entry: {
index: 'src/index.ts',
cli: 'src/cli.ts',
utils: 'src/utils/index.ts',
},
format: ['cjs', 'esm'],
dts: true,
external: ['react'], // Don't bundle peer dependencies
});
tsup Features
export default defineConfig({
entry: ['src/index.ts'],
format: ['cjs', 'esm'],
dts: true,
minify: true, // Minify output
treeshake: true, // Remove dead code
shims: true, // Add CJS/ESM shims for interop
onSuccess: 'node dist/index.cjs', // Run after build
banner: {
js: '// My Library v1.0.0',
},
define: {
'process.env.NODE_ENV': '"production"',
},
});
unbuild: Rollup-Based Library Bundling
Package: unbuild
Weekly downloads: 800K
GitHub stars: 2.5K
Creator: UnJS (Nuxt team)
Underlying: Rollup + mkdist
unbuild is the choice when output optimization matters more than build speed. Rollup's tree-shaking and code splitting are superior to esbuild's for complex libraries.
npm install -D unbuild
// build.config.ts
import { defineBuildConfig } from 'unbuild';
export default defineBuildConfig({
entries: ['./src/index'],
declaration: true,
rollup: {
emitCJS: true,
cjsBridge: true,
esbuild: { minify: true },
},
externals: ['react', 'react-dom'],
});
unbuild automatically infers your build configuration from package.json exports:
// package.json — unbuild reads this automatically
{
"exports": {
".": {
"import": "./dist/index.mjs",
"require": "./dist/index.cjs"
}
}
}
Why Choose unbuild Over tsup?
- Rollup tree-shaking is more aggressive (better output for complex libraries)
mkdistfor distributing TypeScript source files (.d.ts+.ts)- Better handling of CSS-in-JS and asset imports in library code
- Part of the UnJS ecosystem (works with Nuxt, Nitro, H3)
pkgroll: Explicit Entry Points
Package: pkgroll
Weekly downloads: 100K
GitHub stars: 1.5K
Underlying: Rollup
pkgroll reads your package.json exports field to determine what to build — no separate config file needed:
npm install -D pkgroll typescript
// package.json — pkgroll uses this as its configuration
{
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js",
"require": "./dist/index.cjs"
},
"./utils": {
"types": "./dist/utils.d.ts",
"import": "./dist/utils.js"
}
},
"scripts": {
"build": "pkgroll"
}
}
# Just run:
npx pkgroll
# With watch mode:
npx pkgroll --watch
pkgroll's philosophy: your package.json is the source of truth for what gets built. No separate build config file.
esbuild: The Raw Transpiler
Package: esbuild
Weekly downloads: 32M
GitHub stars: 38K
For scripts and non-library code, raw esbuild is fast and simple:
// build.ts — manual esbuild
import * as esbuild from 'esbuild';
await esbuild.build({
entryPoints: ['src/index.ts'],
bundle: true,
outfile: 'dist/index.js',
platform: 'node',
target: 'node20',
format: 'esm',
minify: true,
sourcemap: true,
external: ['express', 'zod'], // Don't bundle dependencies
});
esbuild Watch Mode
const ctx = await esbuild.context({
entryPoints: ['src/index.ts'],
bundle: true,
outdir: 'dist',
platform: 'node',
});
await ctx.watch();
console.log('Watching...');
SWC: Rust-Powered Transpilation
Package: @swc/core
Weekly downloads: 15M
GitHub stars: 32K
SWC is used internally by Next.js, Rspack, and Vite (via plugins). For direct use:
npm install -D @swc/core @swc/cli
// .swcrc
{
"jsc": {
"parser": {
"syntax": "typescript",
"tsx": true,
"decorators": true
},
"transform": {
"react": { "runtime": "automatic" }
},
"target": "es2022"
},
"module": {
"type": "es6"
},
"sourceMaps": true
}
# Transpile a file
npx swc src/index.ts -o dist/index.js
# Transpile directory
npx swc src -d dist
Performance Comparison
| Tool | Transpilation (100 files) | Type Checking | Tree Shaking |
|---|---|---|---|
| tsc | ~10s | Yes | No |
| esbuild | ~0.2s (50x) | No | Basic |
| SWC | ~0.5s (20x) | No | Basic |
| tsup (esbuild) | ~0.3s | Via tsc | Basic |
| unbuild (Rollup) | ~2s | Via tsc | Excellent |
The Recommended Stack
For Library Development
# Build tool: tsup or unbuild
npm install -D tsup typescript
# Type check separately:
# tsc --noEmit (in CI, pre-commit, IDE)
# tsup builds with dts: true (generates .d.ts only, no type errors)
For Application Development
Use your framework's built-in tools:
- Next.js: SWC (built-in) + turbopack
- Vite: esbuild (dev) + Rollup (prod)
- Remix: esbuild
For CLI Tools
# tsup with Node.js target:
tsup src/cli.ts --format cjs --dts --no-splitting
For Monorepos (TypeScript Project References)
# tsc project references for type checking:
tsc --build --verbose
# tsup for each package's output:
workspace: each package has its own tsup.config.ts
Choosing the Right Tool
| Use Case | Recommended Tool |
|---|---|
| NPM library (fast build) | tsup |
| NPM library (optimal bundle) | unbuild |
| NPM library (package.json-driven) | pkgroll |
| Type checking (always) | tsc --noEmit |
| Node.js script | esbuild direct or tsup |
| Next.js app | Built-in SWC + Turbopack |
| Vite app | Built-in esbuild + Rollup |
| Running TS directly | tsx |
Compare download trends for these tools on PkgPulse.
Common TypeScript Build Mistakes
Even experienced TypeScript developers make these mistakes repeatedly. Getting build configuration right the first time saves hours of debugging.
Using tsc for production builds. It's tempting to keep everything in one tool, but tsc compiles TypeScript source files one-to-one — it doesn't bundle, doesn't tree-shake, and includes all node_modules imports as-is. For library publishing, this means consumers get unbundled source with all your dev dependencies potentially visible. For applications, you skip the bundling step entirely. Use tsup or unbuild for libraries; use your framework's bundler for applications.
Not generating declaration maps alongside .d.ts files. Declaration files (.d.ts) let TypeScript consumers get type information. Declaration maps (.d.ts.map) let them "Go to Definition" and land in your TypeScript source rather than the compiled output. Add "declarationMap": true to your tsconfig.json and include sourcemap: true in your tsup/unbuild config. This is especially important for open-source libraries where consumers debug into your code.
Bundling peer dependencies. If your library depends on React, bundling React into your output means consumers end up with two copies of React — yours and the application's — leading to obscure runtime errors. Always mark peer dependencies as external in your build config:
// tsup.config.ts
export default defineConfig({
entry: ['src/index.ts'],
format: ['esm', 'cjs'],
external: ['react', 'react-dom', 'zod'], // Never bundle these
});
Forgetting to add "sideEffects": false to package.json. Without this field, bundlers like Webpack and Rollup assume all your library's modules have side effects and will include them even if they're never imported. Add "sideEffects": false to package.json for pure libraries to enable effective tree-shaking.
Not testing the published package before releasing. The package you publish often differs from the source you develop. Use npm pack to create the tarball locally, then inspect it with tar -tf <tarball>.tgz to verify the dist/ directory is included and no sensitive files leaked. Better yet, use publint to validate your package's exports configuration before every release.
Mismatched moduleResolution settings. TypeScript 5.x introduced "moduleResolution": "bundler" for projects using a bundler. Using the older "node" resolution while targeting modern ESM produces confusing type errors about imports. Use "bundler" for library and application projects using tsup/Vite/esbuild, and "node16" or "nodenext" only for Node.js projects that run directly without bundling.
Dual Package Hazard and the CJS/ESM Split
The biggest ongoing pain in TypeScript library publishing is supporting both CommonJS and ES Modules consumers. The "dual package hazard" occurs when a bundler (or Node.js) loads both the CJS and ESM version of your package simultaneously — because they're treated as separate modules, any package-level state (singletons, module-level variables) is duplicated.
The practical solution for most libraries:
// package.json — correct dual-format exports
{
"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"
}
}
}
}
Note the separate .d.cts type declaration for CJS consumers — TypeScript 4.7+ resolves different declaration files for CJS and ESM imports. tsup generates these automatically when you set format: ['esm', 'cjs'] with dts: true.
For libraries that maintain state (plugin registries, event emitters, global stores), consider shipping ESM-only. Accept that some CJS consumers will need to use dynamic import(). The complexity of dual-format publishing is often not worth it for these packages.
Advanced tsup Patterns
tsup handles most library use cases with minimal config, but several patterns come up repeatedly in production libraries.
Building for Multiple Platforms
Some libraries need different builds for Node.js and browsers:
// tsup.config.ts — multiple build targets
import { defineConfig } from 'tsup';
export default defineConfig([
// Node.js build
{
entry: ['src/index.ts'],
format: ['cjs', 'esm'],
platform: 'node',
target: 'node18',
dts: true,
outDir: 'dist/node',
},
// Browser build (bundled, minified)
{
entry: ['src/index.ts'],
format: ['esm', 'iife'],
platform: 'browser',
target: 'es2020',
minify: true,
globalName: 'MyLibrary', // For IIFE global
outDir: 'dist/browser',
},
]);
Running Scripts After Build
tsup's onSuccess hook is useful for post-build steps like copying assets or running validation:
export default defineConfig({
entry: ['src/index.ts'],
format: ['esm', 'cjs'],
dts: true,
onSuccess: async () => {
// Run publint after every build
const { execSync } = await import('child_process');
execSync('npx publint', { stdio: 'inherit' });
},
});
Watch Mode with Live Reload
For developing libraries alongside an application (in a monorepo or via npm link):
# Terminal 1: Watch and rebuild the library
cd packages/my-lib && npx tsup --watch
# Terminal 2: The application that imports the library
cd apps/my-app && npm run dev
# Vite/Next.js picks up the rebuilt dist/ files automatically
FAQ
Do I need to run tsc separately if tsup generates .d.ts files?
Yes, for type correctness. tsup generates .d.ts files using the TypeScript compiler in an isolated mode that skips full type checking for speed. Running tsc --noEmit separately is the only way to catch type errors. In CI, run both: tsup for output generation and tsc --noEmit for type validation.
When should I use raw esbuild instead of tsup?
When you need fine-grained control over the build process — custom plugins, complex transformation pipelines, or non-library use cases like building scripts or Lambda functions. tsup is an abstraction over esbuild; if you're fighting against its opinions, use esbuild directly.
Is SWC worth using directly in 2026?
For most projects, no. SWC is most valuable as an internal engine (Next.js, Rspack use it). For direct use, tsup (esbuild-based) is simpler to configure and the speed difference between esbuild and SWC is negligible for typical library build sizes. Consider SWC directly if you need decorator support (SWC's decorator handling is more complete than esbuild's) or if you're building Rspack plugins.
How do I publish a TypeScript library to npm?
Build with tsup (tsup src/index.ts --format esm,cjs --dts), set the correct exports in package.json, add "files": ["dist"] to only include the build output, and run npm publish. Use npm pack --dry-run first to verify the package contents. Consider publint as a pre-publish check for common export configuration mistakes.
What's tsx and when should I use it?
tsx is a CLI tool that runs TypeScript files directly using esbuild — no separate build step needed. It's the ts-node replacement for 2026. Use it for scripts, CLI tools in development, and any situation where you want to run TypeScript without compiling first. It does not type check (like all esbuild-based tools), so use tsc --noEmit separately for validation.
Publishing a TypeScript Library: End-to-End Workflow
Knowing which build tool to use is only part of publishing a TypeScript library. Here's the complete workflow from development to npm release.
1. Set up your tsconfig correctly. Use strict settings from the start — "strict": true, "noUncheckedIndexedAccess": true, "exactOptionalPropertyTypes": true. These catch real bugs at compile time. Set "moduleResolution": "bundler" if you're using tsup or unbuild, and "declaration": true, "declarationMap": true for correct type output.
2. Define your public API in src/index.ts. This is what consumers import when they write import { foo } from 'your-library'. Be deliberate about what you export — every exported name is a public API commitment. Types that are used internally but not part of the public API should stay unexported.
3. Configure tsup for dual output. Most libraries should ship both ESM (.js) and CJS (.cjs) for maximum compatibility. The dts: true option generates type declarations automatically. In 2026, some library authors are shipping ESM-only to avoid the dual package hazard — evaluate based on your target audience's environment.
4. Set package.json exports correctly. The exports field controls what consumers can import. Without it, consumers can import any file in your package, including internal implementation details. With it, you control the surface area precisely. The types conditional in exports ensures TypeScript resolves the right declaration files for each module format.
5. Add "files": ["dist"] to package.json. Without this, npm publish includes everything in your project directory — source files, test files, documentation, configuration. The files array whitelist ensures only the built output (and package.json / README) is included in the published package.
6. Validate before publishing. Use publint to check your package configuration:
npx publint
This catches common issues: missing exports, incorrect types paths, main pointing to a file that doesn't exist in exports, and more. It's particularly good at catching the subtle incompatibilities that cause Cannot find module errors for consumers.
7. Use Changesets or np for release automation. Manual npm publish is error-prone. np is a simple CLI that runs your tests, bumps the version, creates a git tag, and publishes in one command. For monorepos publishing multiple packages, changesets provides a more complete workflow with changelogs and coordinated version bumping.
Performance Considerations for Build Tools
The build speed differences between tools are real, but their impact depends on your workflow.
For library development in watch mode, speed matters a lot — you want to see the effect of a change in the consuming application immediately. tsup's watch mode (backed by esbuild) rebuilds in under 100ms for most libraries, which is fast enough that you never notice the rebuild happening. unbuild's watch mode (Rollup-based) is slower — typically 500ms-2s for a medium-sized library — which is noticeable during rapid iteration.
For CI builds, the absolute build time matters less than you might think. A library build that takes 5 seconds (unbuild's Rollup-based approach) vs 0.5 seconds (tsup's esbuild approach) is a 4.5-second difference in a CI pipeline that likely takes minutes overall. The quality of the output (tree-shaking, bundle size) may matter more for your consumers than your CI speed.
For monorepos with many packages, the cumulative effect of per-package build times becomes significant. If you have 20 packages each taking 3 seconds to build, that's a minute of serial build time (though Turborepo/Nx will parallelize this across CPU cores). In this context, tsup's speed advantage compounds and becomes meaningful.
Type checking is the actual bottleneck in most TypeScript projects. tsc --noEmit on a large project with complex types can take 30-60 seconds. This is where investment pays off: TypeScript's incremental and composite options, combined with project references in a monorepo, can cut type-checking time by 70-80% by only rechecking changed files and their dependents.
See also: esbuild vs SWC and esbuild vs Vite, Best TypeScript-First Build Tools 2026.
See the live comparison
View best typescript build tools on PkgPulse →