tsx vs ts-node vs esbuild 2026: TypeScript Runner
TL;DR
tsx for running TypeScript scripts and development workflows. ts-node when you need genuine type-checking at runtime. esbuild for build pipelines and compilation, not interactive script execution. tsx uses esbuild under the hood — it's 10-20x faster to start than ts-node's full TypeScript compilation, with near-zero configuration. ts-node remains the only option that actually type-checks your code at runtime, which matters for CI validation. esbuild is the fastest raw transformer but requires more plumbing to use as a script runner. For node scripts/seed.ts or ts-node src/app.ts in 2026, tsx wins the startup time argument decisively.
Quick Comparison
| tsx v4 | ts-node v10 | esbuild v0.24 | |
|---|---|---|---|
| Weekly Downloads | ~8M | ~16M | ~25M |
| GitHub Stars | ~10K | ~13K | ~38K |
| Startup Time | ~50ms | ~500ms+ | ~20ms (bundler) |
| Type Checking | No (transpile only) | Yes (full) | No (transpile only) |
| ESM Support | Native | Requires config | Native |
| Watch Mode | tsx watch | ts-node-dev (separate pkg) | External (Nodemon) |
| Config Required | None | tsconfig.json needed | Build script needed |
| Node.js Native ESM | --import tsx | --loader ts-node/esm | Requires wrapper |
| Sourcemaps | Yes | Yes | Yes |
| REPL | tsx repl | ts-node (built-in) | No |
Startup Speed: Where tsx Wins
The most measurable difference between tsx and ts-node is startup time, and it matters more than it sounds for developer workflows.
ts-node invokes the full TypeScript compiler to type-check your code before executing it. On a mid-size project with a populated node_modules and many imports, this can take 500ms to 2+ seconds on the first run. For a quick script or a database seed file, that's a noticeable wait on every execution.
tsx uses esbuild as its transformer — it strips TypeScript syntax and converts it to JavaScript without checking types. The startup time drops to ~50ms, which feels instant compared to ts-node's full compilation.
# Startup time comparison (hello world TypeScript file):
time ts-node src/hello.ts # 0.52s real
time tsx src/hello.ts # 0.05s real
# For a file with 20 imports across a real project:
time ts-node src/seed.ts # 2.1s real
time tsx src/seed.ts # 0.08s real
For running scripts interactively — database seeds, migrations, one-off jobs, development utilities — this difference changes your workflow. With ts-node, you start mentally accounting for the wait. With tsx, you run scripts as freely as you'd run node file.js.
The tradeoff is type safety: tsx won't catch a type error before execution. If your seed script passes a string where a number is expected, tsx will run it and fail at runtime; ts-node would have caught it before the first line executed.
Type Checking: The Only Reason to Keep ts-node
This is where ts-node's case is unambiguous: it's the only runner that actually checks your TypeScript types.
# This TypeScript has a type error:
# const x: number = "not a number";
ts-node src/broken.ts
# Error: Type 'string' is not assignable to type 'number'. (TS2322)
# Process exits without executing
tsx src/broken.ts
# Runs fine — tsx ignores the type error, executes the JS
# May fail later at runtime depending on how the value is used
For most development scripts, this doesn't matter — you want fast execution and you'll catch type errors in your IDE or CI. But there are scenarios where runtime type-checking is genuinely valuable:
- CI validation scripts: Running
ts-node verify-config.tsin CI catches type errors before deployment - Build verification: Type-checking the build system itself
- Complex data transformation scripts where type safety is safety-critical
For these use cases, ts-node's slower startup is the price you pay for the guarantee that types are correct before execution. If you're using tsc --noEmit in CI already, you probably don't need ts-node's type-checking on top — tsx is faster and your type safety is handled separately.
A common hybrid approach: use tsx for development (fast iteration), add tsc --noEmit to CI (type verification), and never run ts-node in production.
esbuild as a TypeScript Runner
esbuild is primarily a bundler and transpiler, not a script runner. But it's worth addressing because it's often mentioned alongside tsx — tsx actually uses esbuild internally.
You can use esbuild to transpile TypeScript to JavaScript and then execute the output, but it requires more wiring than tsx or ts-node:
# Using esbuild to run a TypeScript file:
esbuild src/script.ts --bundle --platform=node --outfile=dist/script.js && node dist/script.js
# Or pipe it directly (no type-checking):
node --input-type=module < <(esbuild --bundle src/script.ts --platform=node --format=esm)
Neither of these is ergonomic for daily use. tsx wraps esbuild's transformer into a drop-in node replacement, handling module interop and loader registration automatically. When people say "use esbuild to run TypeScript," they usually mean "use tsx, which is powered by esbuild."
Where esbuild genuinely shines as a standalone tool:
- Build pipelines: Transpiling and bundling TypeScript for production
- Custom scripts:
esbuild.config.tsbuild scripts - Monorepo compilation: Fast per-package builds without a full tsconfig cascade
# esbuild at its best — building a package:
esbuild src/index.ts \
--bundle \
--platform=node \
--format=cjs \
--outfile=dist/index.js \
--external:express,pg
# Fast, deterministic, excellent for CI builds
For this use case, esbuild's ~25M weekly downloads make sense — many of those are build tools (Vite, tsup) using esbuild as their underlying transform engine, not developers running scripts.
ESM and CommonJS Compatibility
All three tools have different stories for ESM module support, and this matters in 2026 where many packages have dropped CommonJS.
tsx handles ESM transparently. You can use import/export in your TypeScript files and tsx resolves them correctly without configuration:
# ESM-first usage with tsx
tsx src/app.ts # CJS and ESM scripts both work
node --import tsx src/app.ts # Register tsx as loader for native Node.js ESM
ts-node requires explicit ESM configuration:
// tsconfig.json — needed for ts-node ESM mode
{
"compilerOptions": {
"module": "ESNext",
"moduleResolution": "bundler"
}
}
# ts-node ESM mode is a separate command:
node --loader ts-node/esm src/app.ts
# Or: ts-node --esm src/app.ts
The ts-node ESM experience improved significantly in v10, but it still requires more configuration than tsx. In mixed CJS/ESM projects, tsx's transparent handling saves meaningful setup friction.
Watch Mode and Development Experience
For active development with file watching, each tool has a different story:
# tsx — built-in watch mode, no extra package:
tsx watch src/server.ts
# ts-node — requires ts-node-dev (separate npm package):
npm install -D ts-node-dev
ts-node-dev --respawn src/server.ts
# esbuild — no native watch-and-run; use nodemon:
npm install -D nodemon
nodemon --exec 'esbuild src/server.ts --platform=node | node' src/
tsx's built-in watch mode eliminates a dependency. ts-node-dev has been the standard solution for ts-node watch mode for years, but it's a separate package that needs maintaining and has its own edge cases. esbuild has no built-in "run and watch" mode for scripts.
When to Use Which
Choose tsx when:
- You run TypeScript scripts frequently during development
- You want drop-in replacement for
nodewithout configuration - ESM compatibility matters and you want it to "just work"
- You use a modern stack and already run
tscseparately for type checking - You're replacing
ts-nodein a new project
Choose ts-node when:
- You need runtime type-checking before execution
- You're using it in CI to validate scripts (not just transpile)
- Your toolchain specifically requires ts-node (some legacy integrations)
- You want the TypeScript REPL (
ts-nodestarts an interactive REPL)
Choose esbuild (standalone) when:
- You're building a production bundle or library
- You're writing build tooling or custom compilation steps
- You need maximum speed for transpilation-only pipelines
- You're building a tool that wraps esbuild (like tsup does)
Migration: ts-node to tsx
For most projects, switching from ts-node to tsx is a package.json change:
// Before:
{
"scripts": {
"dev": "ts-node src/server.ts",
"seed": "ts-node scripts/seed.ts"
}
}
// After:
{
"scripts": {
"dev": "tsx watch src/server.ts",
"seed": "tsx scripts/seed.ts"
}
}
npm uninstall ts-node
npm install -D tsx
Edge cases to check:
- If you use
require()hooks via ts-node's register API, tsx has--import tsxas the Node.js equivalent - If you rely on ts-node's type-checking in CI scripts, add
tsc --noEmitas a separate CI step tsconfig.jsonis respected by both tools, so no tsconfig changes needed
For projects running Node.js 22+ with native --experimental-strip-types, there's a third option: run TypeScript directly without any loader at all for simple scripts with no decorators or const enums. But tsx remains the most reliable solution for the general case.
See also: tsx on PkgPulse, best TypeScript build tools for 2026, and tsx vs ts-node vs Bun 2026.