TypeScript with esbuild and swc: Comparing Fast Compiler Alternatives
AI generated
<T>
type
TypeScript · esbuild · swc · Build Performance
TypeScript with esbuild and swc
native compiler speed without the tsc bottleneck

The official TypeScript compiler tsc is written in JavaScript and optimized for type safety, not speed. esbuild and swc compile the same TypeScript code in Go and Rust respectively, achieving ten to twenty times shorter build times, while deliberately skipping full type checking during transformation.

18 min read isolatedModules · tsc --noEmit · esbuild · @swc/core TypeScript 5.x · Node.js 20/22

1. Why tsc becomes a bottleneck for large projects

The official TypeScript compiler tsc is itself implemented in TypeScript or JavaScript, and on every build it has to go through the complete type-checking process: it resolves module dependencies, builds a type tree for the entire project, and checks every assignment, function call and interface implementation against that type system. For small projects with a few hundred files, this is not a problem, but for large codebases with several thousand files, a single tsc build can take several minutes, which adds up to significant delays in a CI pipeline with hundreds of builds per day.

The insight that led to the development of esbuild and swc is simple: the actual transformation from TypeScript syntax to JavaScript, meaning stripping type annotations and translating newer language features, is computationally trivial and does not require a full type check. Only the type check itself is the expensive part. If both steps are separated, the pure transformation can run orders of magnitude faster in a natively compiled language like Go or Rust than in JavaScript, while type checking continues to run separately with tsc as needed.

2. The core principle: transpiling instead of type checking

Both esbuild and swc work on the same core principle that TypeScript itself offers via its transpileModule API: each file is parsed on its own and its type annotations are stripped, without actually validating the type against other files in the project. A call to a method that does not exist on an object is compiled without complaint as long as the syntax itself is valid TypeScript. The error would only become visible at runtime as a TypeError, not already at build time.

This fundamental difference from tsc is not a bug but a deliberate design goal: esbuild and swc aim to turn TypeScript source code into runnable JavaScript as fast as possible, while delegating the task of type safety to a separate process. In practice, this means a production setup almost always uses two tools in parallel: a fast transpiler for the actual build, and tsc exclusively for type checking, usually as its own CI step or as an IDE integration.

3. esbuild in detail: Go performance for TypeScript transforms

esbuild, written in Go by Evan Wallace, is primarily a bundler with a built-in TypeScript transpiler that combines parsing, transforming and bundling into a single, highly parallelized process. The performance gains come from several sources at once: Go compiles to native machine code without the interpreter overhead of Node.js, esbuild uses all available CPU cores in parallel for independent files, and the internal AST is kept as a memory-efficient, compact data structure instead of a generic JavaScript object graph.

For TypeScript projects, the esbuild API is usable both via the CLI and programmatically through Node.js, making it the preferred choice for custom build scripts. Its configuration stays deliberately minimal compared to Webpack, but it also forgoes more complex features like a plugin ecosystem of comparable scale. For pure transpilation and bundling tasks in TypeScript projects, esbuild is therefore often the most pragmatic choice when no deep custom transformations of the code are needed.


// build.mjs — esbuild programmatic build script for a TypeScript service
import * as esbuild from 'esbuild';

const result = await esbuild.build({
  entryPoints: ['src/main.ts'],
  bundle: true,
  platform: 'node',
  target: 'node20',
  format: 'esm',
  outfile: 'dist/main.js',
  sourcemap: 'linked',
  minify: process.env.NODE_ENV === 'production',
  external: ['pg-native'], // native modules stay external
  metafile: true,
});

// Optional: inspect bundle composition for size regressions
console.log(await esbuild.analyzeMetafile(result.metafile));

4. swc in detail: Rust-based compilation with a plugin system

swc (Speedy Web Compiler), written in Rust, follows an approach similar to esbuild, but differs in two important ways: it offers a more extensive, WASM-based plugin system for custom AST transformations, and it is used by larger frameworks like Next.js as a replacement for Babel, which makes it more deeply integrable into existing transform pipelines. While esbuild primarily acts as a standalone bundler, swc positions itself more as an interchangeable compiler engine within other tools.

For TypeScript projects that need custom code transformations in addition to pure transpilation, such as automatically stripping certain decorators or inserting instrumentation code for observability, swc offers considerably more flexibility than esbuild through its plugin system. This flexibility comes with a somewhat more complex configuration via the .swcrc file, which carries its own JSON structure and conventions compared to esbuild's JavaScript configuration object.


{
  "jsc": {
    "parser": {
      "syntax": "typescript",
      "tsx": false,
      "decorators": true
    },
    "target": "es2022",
    "transform": {
      "legacyDecorator": true,
      "decoratorMetadata": true
    },
    "keepClassNames": true
  },
  "module": {
    "type": "commonjs"
  },
  "minify": false,
  "sourceMaps": true
}

5. isolatedModules: the necessary tsconfig safeguard

Because esbuild and swc transpile every file in isolation, without knowing the context of other files in the project, certain TypeScript features that rely on cross-file type information must be avoided. The most important example is const enum, which the full tsc compiler replaces with literal values across all files at compile time, while an isolated transpiler cannot reliably perform this substitution without the overall context.

The "isolatedModules": true flag in tsconfig.json activates exactly the checks that ensure the TypeScript code can also be transpiled correctly without cross-file context. It warns, for instance, about re-exporting pure types without the export type keyword, since an isolated transpiler cannot tell whether a re-exported identifier is a type or a value, and would in doubt generate a runtime import that fails at runtime. Every project using esbuild or swc instead of tsc for the actual build should enable isolatedModules without exception.


// isolatedModules requires explicit "export type" for pure type re-exports
// WRONG: ambiguous for an isolated transpiler — is UserRole a type or a value?
export { UserRole } from './types';

// RIGHT: explicit type-only re-export, safe for esbuild and swc
export type { UserRole } from './types';

// const enum is NOT allowed with isolatedModules — needs cross-file inlining
// WRONG:
// const enum Status { Active, Inactive }

// RIGHT: regular enum works fine with isolated transpilers
enum Status {
  Active = 'ACTIVE',
  Inactive = 'INACTIVE',
}

6. Running type checking separately: tsc --noEmit

Since esbuild and swc deliberately do not perform type checking, tsc with the --noEmit option remains the standard way to still guarantee full type safety in the project, without tsc itself producing the actual build output. This command runs the complete type-checking process, reports all type errors, but does not generate .js files, since the actual transpilation is already handled by the faster tool.

In practice, tsc --noEmit usually runs as a separate CI step in parallel with the actual build, or as a local pre-commit hook for the developer, while the fast transpiler is responsible for the daily development loop and the actual deployment. This split means a type error does not immediately block the local development server, which many teams consider an acceptable trade-off, as long as the CI build reliably prevents merging into the main branch when type errors are present.


#!/usr/bin/env bash
# ci.sh — fast build with esbuild, type safety verified in parallel
set -euo pipefail

# Run both steps concurrently, fail the pipeline if either one fails
node build.mjs &
BUILD_PID=$!

npx tsc --noEmit &
TYPECHECK_PID=$!

wait "$BUILD_PID" && wait "$TYPECHECK_PID"
echo "[OK] Build and type check both passed"

7. Integration into Vite, Jest and build pipelines

Vite already uses esbuild by default to transpile TypeScript files during development, which is one of the main reasons for Vite's noticeably shorter cold-start times compared to Webpack-based setups. For production builds, Vite switches to Rollup by default for the actual bundling, but continues to use esbuild for the pure TypeScript-to-JavaScript transformation of individual modules before Rollup merges them.

For test frameworks like Jest, @swc/jest replaces the slower default ts-jest transformer and considerably speeds up test suites with many small test files, since each file is transpiled without the overhead of a full type check. Vitest also uses esbuild internally for transformation, which makes it one of the fastest options for TypeScript test suites out of the box, without requiring additional configuration.


{
  "scripts": {
    "build": "node build.mjs",
    "typecheck": "tsc --noEmit",
    "test": "jest",
    "ci": "npm run typecheck && npm run build && npm run test"
  },
  "devDependencies": {
    "esbuild": "^0.23.0",
    "@swc/core": "^1.7.0",
    "@swc/jest": "^0.2.36",
    "jest": "^29.7.0",
    "typescript": "^5.6.2"
  },
  "jest": {
    "transform": {
      "^.+\\.tsx?$": ["@swc/jest"]
    }
  }
}

8. Limits: what esbuild and swc cannot do

Besides the missing type check, both esbuild and swc have additional limitations that become relevant for certain TypeScript projects. Neither tool can generate declaration files (.d.ts), since generating correct type definitions requires a full type check. Projects published as npm libraries with type definitions therefore still need a separate tsc run with "declaration": true, even if the actual JavaScript output comes from esbuild or swc.

Another difference concerns experimental decorators and metadata reflection, as used by frameworks like NestJS or TypeORM. swc explicitly supports decoratorMetadata through its configuration and is usually the more reliable choice for such frameworks, while esbuild transpiles experimental decorators but without the full metadata emission that some dependency-injection frameworks need at runtime. Before switching from tsc to one of the two native compilers, it is therefore worth testing the entire decorator chain in the specific project.

9. esbuild, swc and tsc directly compared

The choice between these three tools depends on the concrete requirements of the project. The following table compares the most important properties for TypeScript build setups.

Property tsc esbuild swc
Build speed Baseline (1x) 10 to 20x faster 10 to 20x faster
Full type checking Yes No No
.d.ts generation Yes No No
Plugin system No (transformer API) Simple, JS-based Extensive, WASM-based
Decorator metadata Full Limited Fully supported
Typical use Type checking, library builds Bundling, Vite, fast scripts Next.js, Jest transform, NestJS

In practice, these three tools do not exclude each other but complement one another in a well-configured TypeScript setup: esbuild or swc handle the actual, frequently repeated transformation during development and deployment, while tsc --noEmit, running as a separate, less frequent step, guarantees type safety. For libraries with published type definitions, a dedicated tsc run with the declaration option enabled remains additionally necessary.

Mironsoft

Build performance, CI optimization and TypeScript tooling

Ready to noticeably speed up multi-minute TypeScript builds?

We analyze your existing tsc-based build pipeline, introduce esbuild or swc where it makes sense, and set up a clean separation between fast builds and dedicated type checking.

Build audit

Measuring current build times and identifying the biggest bottlenecks

Compiler migration

Switching to esbuild or swc including isolatedModules safeguards

CI integration

Separate, parallel tsc --noEmit step for reliable type safety

10. Summary

esbuild and swc solve a real performance problem for production TypeScript projects by separating the transformation from TypeScript to JavaScript from the expensive type check and running it orders of magnitude faster in a natively compiled language than the JavaScript-based tsc. This speed comes at a price: both tools transpile files in isolation, without cross-file type checking, and therefore require "isolatedModules": true in tsconfig.json as well as a separate tsc --noEmit step for full type safety.

esbuild is particularly well suited for bundling and fast custom build scripts, while swc, with its more extensive plugin system and full decorator metadata support, is the better choice for frameworks like NestJS or Next.js. Neither tool fully replaces tsc, they complement it: the fast compiler handles the daily build, while tsc remains responsible for type checking and declaration-file generation.

TypeScript with esbuild and swc — Key Takeaways

Transpiling over type checking

esbuild and swc strip type annotations in isolation per file, without cross-file type checking.

isolatedModules mandatory

Prevents const enum and ambiguous type re-exports that isolated transpilers cannot safely resolve.

tsc --noEmit separately

Runs as its own CI step for full type safety, without blocking the fast build.

Tool choice

esbuild for bundling and Vite, swc for plugin needs and decorator-heavy frameworks like NestJS.

11. FAQ: TypeScript with esbuild and swc

1Do esbuild/swc check types?
No, both strip annotations in isolation. Full safety needs a separate tsc --noEmit run.
2Why is isolatedModules mandatory?
Prevents features like const enum that require cross-file context isolated transpilers lack.
3Do they generate .d.ts files?
No, a separate tsc run with declaration: true is still needed.
4How much faster are they?
Ten to twenty times, thanks to native implementation and skipped type checking.
5When swc over esbuild?
With decorator metadata needs like NestJS or when a richer plugin system is required.
6Does a type error block the dev server?
Not with esbuild, but CI should still safeguard via a separate tsc step.
7Vite with esbuild in production?
esbuild transforms modules, Rollup handles final bundling by default.
8Is @swc/jest faster than ts-jest?
Yes, significantly, since ts-jest performs real type checking on every run.
9const enum with esbuild/swc?
Not possible, isolatedModules reports an error, use regular enum instead.
10Remove tsc entirely?
No, it remains indispensable for type checking and declaration files.