Simplify TypeScript builds with esbuild and skip tsc/tsx

The "esbuild over tsc" pitch has narrowed since 2023 — Node 22 added --experimental-strip-types, Node 23+ runs TypeScript files directly with no flag, and Bun and Deno have always done this natively. For running TypeScript, the runtime now handles it. But for bundling TypeScript — into a single file, for a specific target, with watch mode and source maps — esbuild is still the right tool. Here's a clean setup for the bundling case.

Install

1npm install esbuild --save-dev 2# or 3bun add -d esbuild

A working script

1{ 2 "scripts": { 3 "build": "esbuild src/index.ts --bundle --platform=node --target=node20 --outdir=build --format=esm --sourcemap", 4 "build:watch": "npm run build -- --watch", 5 "typecheck": "tsc --noEmit", 6 "start": "node build/index.js", 7 "dev": "concurrently \"npm:build:watch\" \"node --watch build/index.js\"" 8 } 9}

What each flag does

  • --bundle — collapse all imports into a single output. Without it, esbuild compiles file-by-file.
  • --platform=node — emit code targeting Node (vs browser or neutral).
  • --target=node20 — pin to a runtime version's supported syntax. --target=esnext can emit features the runtime doesn't grok yet; better to be explicit.
  • --outdir=build — destination directory.
  • --format=esm — ES modules. Use cjs if you're shipping to a CommonJS-only consumer (still common in Node tooling).
  • --sourcemap — emit .js.map files. Without these, every stack trace points to the bundled output instead of your TypeScript source. Always-on in any non-trivial project.
  • --watch — rebuild on file changes. Pair with node --watch for a full hot-reload loop.

The thing esbuild doesn't do

esbuild does not type-check. It strips TypeScript syntax and emits JavaScript at C-speed, but it never looks at your types. A wrong as Foo or a missing property gets through silently.

Keep tsc --noEmit in CI as a separate step from build:

1"typecheck": "tsc --noEmit"

CI fails fast on type errors; local dev keeps moving fast on the build itself.

Decorators with emitDecoratorMetadata

If you're on NestJS, TypeORM, or anything that depends on emitted decorator metadata, esbuild handles experimentalDecorators but not emitDecoratorMetadata natively. Either run the metadata-dependent paths through tsc or use a plugin like esbuild-plugin-typescript-decorators. It's the one place esbuild's strip-types approach falls short.

When to skip esbuild entirely in 2026

The decision tree's gotten simpler:

  • Just running scripts or a dev server? node --experimental-strip-types script.ts (Node 22) or node script.ts (Node 23+). Bun and Deno run TS with no flag at all. No build step.
  • Need a runner with full TS feature support? tsx is the maintained successor to ts-node and works without configuration.
  • Bundling for production, multiple targets, source maps, tree-shaking? esbuild. This is what it's built for, and it's still the fastest option that's not oxc (the Rust-based newcomer that's still under heavy development).
  • Bundling a frontend app? Vite, which uses esbuild under the hood and adds a dev server, HMR, and plugin ecosystem on top.

esbuild is still the tool. The job is just narrower than it used to be.