<!-- PkgPulse AI-readable guide source -->
<!-- Canonical: https://www.pkgpulse.com/guides/best-typescript-build-tools-2026 -->
<!-- Raw Markdown: https://www.pkgpulse.com/guides/best-typescript-build-tools-2026/raw.md -->
<!-- Source path: content/guides/best-typescript-build-tools-2026.mdx -->

---
og_image: "/images/guides/best-typescript-build-tools-2026.webp"
title: "Best TypeScript-First Build Tools 2026"
description: "Compare tsup, unbuild, pkgroll, tsc, esbuild, and SWC for building TypeScript libraries and applications in 2026. Speed benchmarks, DX, and when to use each."
date: "2026-03-08"
author: "PkgPulse Team"
tags: ["typescript", "tsup", "esbuild", "swc", "unbuild", "build-tools", "2026"]
featured_comparison: "best-typescript-build-tools"
---

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 esbuild
- `esbuild`: 45x faster than tsc for transpilation; does NOT type check
- `swc`: 20x faster than tsc; does NOT type check; used by Rspack/Next.js internals
- `tsup` (~1.2M weekly downloads): Most popular library bundler, uses esbuild, zero-config
- `unbuild`: Rollup-based, better for libraries needing optimal tree-shaking
- `pkgroll`: 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:

```bash
# Type check only (no output files)
npx tsc --noEmit

# Or in watch mode during development
npx tsc --noEmit --watch
```

```json
// package.json
{
  "scripts": {
    "type-check": "tsc --noEmit",
    "build": "tsup src/index.ts --format esm,cjs --dts"
  }
}
```

### tsconfig.json for Library Publishing

```json
{
  "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:

```bash
npm install -D tsup
```

### Basic Library Setup

```typescript
// 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
});
```

```json
// 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

```typescript
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

```typescript
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.

```bash
npm install -D unbuild
```

```typescript
// 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:

```json
// 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)
- `mkdist` for 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:

```bash
npm install -D pkgroll typescript
```

```json
// 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"
  }
}
```

```bash
# 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:

```typescript
// 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

```typescript
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:

```bash
npm install -D @swc/core @swc/cli
```

```json
// .swcrc
{
  "jsc": {
    "parser": {
      "syntax": "typescript",
      "tsx": true,
      "decorators": true
    },
    "transform": {
      "react": { "runtime": "automatic" }
    },
    "target": "es2022"
  },
  "module": {
    "type": "es6"
  },
  "sourceMaps": true
}
```

```bash
# 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

```bash
# 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

```bash
# tsup with Node.js target:
tsup src/cli.ts --format cjs --dts --no-splitting
```

### For Monorepos (TypeScript Project References)

```bash
# 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](https://pkgpulse.com).

## 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:

```typescript
// 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:

```json
// 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:

```typescript
// 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:

```typescript
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`):

```bash
# 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:

```bash
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](/compare/esbuild-vs-swc) and [esbuild vs Vite](/compare/esbuild-vs-vite), [Best TypeScript-First Build Tools 2026](/guides/best-typescript-first-build-tools-2026).*

Compiler note: [tsgo vs tsc](/guides/tsgo-vs-tsc-typescript-7-go-compiler-2026) is the canonical guide for TypeScript 7 native compiler adoption.
