tsx vs ts-node vs Bun: Running TypeScript Directly 2026
TL;DR
tsx has replaced ts-node as the default TypeScript runner for Node.js projects. tsx is esbuild-powered (fast), has full ESM support, and works with Node.js 18+. ts-node is 5-10x slower (runs tsc under the hood) and ESM support is historically painful. Bun is the fastest option overall — TypeScript is native, no loader required — but Node.js compatibility issues remain for some production workloads. For scripts and CLIs: tsx or Bun. For production Node.js servers: compile to JavaScript with tsc or tsup, do not run TypeScript directly in prod. ts-node: only for legacy projects that already use it.
Key Takeaways
- tsx: ~2M downloads/week, esbuild-based, ~50ms startup, full ESM + CJS, drop-in ts-node replacement
- ts-node: ~7M downloads/week but declining, tsc-based, ~500ms startup, painful ESM support
- Bun: ~900K downloads/week, native TypeScript runtime, <5ms startup, ~90-95% Node.js compat
- Node.js 22 native:
--experimental-strip-typesfor simple scripts, not a replacement for tsx - Startup times: Bun (<5ms) > tsx (~50ms) > ts-node (~500ms)
- For development: tsx or Bun; avoid ts-node in new projects
Why Running TypeScript Directly Matters
The traditional TypeScript workflow has always been: write .ts files, run tsc to compile, run the compiled .js files. This works perfectly for production deployments where you want the fastest possible startup and no runtime dependencies on TypeScript tooling.
But for development scripts, database migrations, seed scripts, and CLIs, the compile step creates friction. You change a seed script, you have to wait for tsc to compile it, then run the output. Or you set up a ts-node invocation that does both in one command.
The question is which tool does that compile-and-run step most efficiently, and which trade-offs are acceptable for your use case.
The three serious options in 2026 each represent a different philosophy: tsx extends Node.js with esbuild as a loader (fast transpile, no type checking); ts-node extends Node.js with the TypeScript compiler itself (slower, but validates types if configured); and Bun replaces Node.js with a runtime that natively understands TypeScript source files.
tsx — The Node.js Standard (~2M downloads)
tsx is the practical default for running TypeScript in Node.js projects in 2026. It uses esbuild to transpile TypeScript on the fly, stripping types and transforming syntax without running the TypeScript compiler's full type checking pass. The result is startup around 50ms — fast enough that the tool feels instant.
The key insight about tsx is that it does not type check your code. It transpiles: it removes type annotations and converts modern syntax to JavaScript that Node.js can execute. Type checking is a separate step you run with tsc --noEmit in your CI pipeline or as an IDE background task. This separation is what makes tsx fast.
# Install
npm install --save-dev tsx
# Run a TypeScript file
npx tsx script.ts
# Watch mode — re-runs on file changes
npx tsx watch src/server.ts
# Use as a Node.js loader (Node.js 18+ register API)
node --import tsx/esm src/index.ts
tsx works transparently across ESM and CJS projects. In a project with "type": "module" in package.json, it handles ESM correctly. In a CJS project, it handles CJS. You do not need to configure loaders or add ts-node/esm shims.
// src/server.ts — Express with tsx
import express from 'express';
import { db } from './lib/db.js'; // .js extension required in ESM
const app = express();
app.get('/health', (_, res) => {
res.json({ status: 'ok', timestamp: new Date().toISOString() });
});
app.listen(3000, () => console.log('Server running on :3000'));
// package.json — typical tsx setup
{
"scripts": {
"dev": "tsx watch src/server.ts",
"start": "tsx src/server.ts",
"build": "tsc",
"typecheck": "tsc --noEmit",
"seed": "tsx scripts/seed.ts",
"migrate": "tsx scripts/migrate.ts"
},
"type": "module"
}
The --import tsx pattern is useful for integrating TypeScript into environments that run JavaScript but accept loader registration:
# Use with Node.js test runner
node --import tsx --test src/**/*.test.ts
# Use in a Node.js script that spawns subprocesses
NODE_OPTIONS='--import tsx' node scripts/runner.js
tsx is a drop-in replacement for ts-node. Any documentation or script that calls ts-node src/index.ts can be updated to tsx src/index.ts with no other changes needed.
Startup benchmark:
Script startup time (simple TypeScript file, 3-run average):
tsx: ~48ms
ts-node: ~480ms (10x slower — runs full TypeScript compiler)
Bun: ~12ms
Impact for scripts run 100 times per day:
tsx: 4.8 seconds of startup overhead per day
ts-node: 48 seconds of startup overhead per day
ts-node — The Legacy Standard (~7M downloads, declining)
ts-node was the original solution for running TypeScript in Node.js, and its 7 million weekly downloads still reflect how many existing projects and pipelines depend on it. But the trend line is downward — tsx has taken the mindshare for new projects, and ts-node has not released a major version since 2021.
ts-node runs the actual TypeScript compiler, which is why it is slow (around 500ms startup) but also why it historically offered an option for real-time type checking. In practice, most teams disable type checking even in ts-node because the startup penalty compounds across every script invocation:
npm install --save-dev ts-node typescript @types/node
# Standard CJS mode (what most projects use)
npx ts-node src/index.ts
# ESM mode (requires extra setup, historically painful)
npx ts-node --esm src/index.ts
// tsconfig.json — ts-node configuration section
{
"compilerOptions": {
"module": "CommonJS",
"esModuleInterop": true,
"resolveJsonModule": true
},
"ts-node": {
"esm": true,
"experimentalSpecifierResolution": "node",
"transpileOnly": true // Skip type checking for speed
}
}
Setting transpileOnly: true in the ts-node config makes it skip type checking, which brings startup down to around 200ms — still 4x slower than tsx, and with more configuration required to achieve it.
Why ts-node is declining:
- tsx exists and is faster with zero configuration overhead
- ESM support in ts-node requires multiple tsconfig options; tsx handles ESM automatically
- ts-node requires TypeScript as a peer dependency; tsx uses its own esbuild binary
ts-node/esmloader is effectively deprecated in favor of tsx
When to keep ts-node: if your project is already using it and the migration cost is not worth it. If you have ts-node-dev, @swc/register, or ts-jest infrastructure built around ts-node, evaluate whether the speed improvement justifies the migration work on a case-by-case basis. For new projects: use tsx.
Bun — Native TypeScript Runtime (~900K downloads)
Bun is not a Node.js add-on — it is a separate JavaScript runtime built from scratch in Zig, designed to be fast by default. TypeScript support is built into the runtime itself, which means there is no loader to register, no pre-compilation step, and no configuration required. You write TypeScript and run it with bun run.
# Install Bun
curl -fsSL https://bun.sh/install | bash
# Run TypeScript directly
bun run src/script.ts
bun run src/server.ts
# Watch mode
bun --watch run src/server.ts
# Run npm scripts (Bun reads package.json)
bun dev
bun test # Built-in test runner
Bun's startup time is exceptional — below 5ms for simple scripts. For scripts that run frequently in development (file watchers, test setup, DB queries), this makes Bun feel qualitatively different from tsx. You press Enter and the result appears before you have fully shifted attention.
// Bun's native HTTP server (no Express needed)
const server = Bun.serve({
port: 3000,
fetch(req) {
const url = new URL(req.url);
if (url.pathname === '/health') {
return Response.json({ status: 'ok', runtime: 'bun' });
}
if (url.pathname === '/users') {
const users = await db.query('SELECT * FROM users');
return Response.json(users);
}
return new Response('Not Found', { status: 404 });
},
});
console.log(`Listening on ${server.url}`);
You can also run popular frameworks on Bun without using Bun's native APIs:
// Hono on Bun (fully compatible)
import { Hono } from 'hono';
const app = new Hono();
app.get('/health', (c) => c.json({ status: 'ok' }));
app.get('/users', async (c) => {
const users = await db.query('SELECT * FROM users LIMIT 100');
return c.json(users);
});
export default {
port: 3000,
fetch: app.fetch,
};
Bun's Node.js compatibility covers approximately 90-95% of the npm ecosystem. Most packages work. The gaps are in packages that depend on exact Node.js internals, native addons compiled with node-gyp, or specific worker_threads behavior. Before committing to Bun for a production service, verify your complete dependency tree works:
Confirmed working on Bun:
- Hono, Express, Fastify (with minor notes)
- Prisma ORM, Drizzle ORM
- Zod, tRPC, TanStack Query
- AWS SDK v3
- Most cryptography packages
Needs verification:
- Node.js native addons (gyp-compiled)
- Some packages using specific Node stream internals
- Packages that spawn Node.js child processes expecting specific behavior
- Some older packages using deprecated Node.js APIs
Known issues:
- worker_threads behavior differs from Node.js in some cases
- Some spawn/child_process edge cases
Node.js 22+ Native TypeScript
Node.js 22 added --experimental-strip-types, which allows Node.js to run .ts files directly without a separate loader. This landed as a native capability, not an external package:
# Node.js 22+ — run TypeScript natively
node --experimental-strip-types script.ts
The limitations are significant: const enum is not supported (because it requires a full TS compiler pass), decorators require --experimental-transform-types, and type-only syntax must use the correct TypeScript idioms. It works for simple utility scripts but is not a replacement for tsx in projects that use advanced TypeScript features.
The flag signals where Node.js is going: native TypeScript support without external tooling. But "without external tooling" means "without the features those tools provide." For anything beyond the simplest scripts, tsx or Bun will remain the better choice through 2026 and likely beyond.
Scripts vs Production: A Critical Distinction
Running TypeScript directly is appropriate for:
- Development scripts (seed scripts, migration runners, one-off utilities)
- CLI tools distributed to developers
- Development servers during active development
- Test files
Running TypeScript directly is not appropriate for production servers in most cases. The compilation step that tsx and Bun skip in development adds startup overhead in production, and it means you are shipping TypeScript source to production (which is fine) or relying on a runtime loader being present (which adds a runtime dependency).
The recommended pattern for production Node.js services:
{
"scripts": {
"dev": "tsx watch src/server.ts",
"build": "tsup src/server.ts --format esm --dts",
"start": "node dist/server.js",
"typecheck": "tsc --noEmit",
"seed": "tsx scripts/seed.ts",
"test": "vitest"
}
}
tsup (built on esbuild, same as tsx) compiles TypeScript to JavaScript for production. The start script runs compiled JavaScript with no runtime TypeScript dependency. This gives you tsx's speed in development and clean compiled output in production.
For Bun servers where Bun is also the production runtime, this distinction is less important — Bun handles TypeScript natively in both environments and the startup overhead is under 5ms regardless.
Package Health
| Package | Weekly Downloads | Trend | Underlying Engine | Type Checking | Node.js Required |
|---|---|---|---|---|---|
| ts-node | ~7M | ↓ Declining | TypeScript compiler | Optional | Yes |
| tsx | ~2M | ↑ Fast growing | esbuild | No | Yes (18+) |
| bun | ~900K | ↑ Growing | Native (Zig) | No | No (is the runtime) |
Comparison Table
| tsx | ts-node | Bun | |
|---|---|---|---|
| Startup time | ~50ms | ~500ms | <5ms |
| TypeScript execution | esbuild (transpile only) | tsc (optional type check) | Native |
| Type checking | ❌ (run tsc separately) | Optional | ❌ (run tsc separately) |
| ESM support | ✅ Full | ⚠️ Painful | ✅ Full |
| CJS support | ✅ Full | ✅ Full | ✅ Full |
| Node.js compat | 100% | 100% | ~90-95% |
| Watch mode | ✅ tsx watch | ⚠️ ts-node-dev | ✅ bun --watch |
| Config needed | ❌ None | ✅ tsconfig additions | ❌ None |
| npm packages | All | All | ~95% |
| Production use | Compile with tsup | Compile with tsc | Bun runtime |
When to Choose
tsx is the default for Node.js projects. It is fast, requires zero configuration, handles ESM and CJS transparently, and is a drop-in replacement for ts-node in any existing script. If you are on Node.js and want to run TypeScript directly, tsx is the answer.
Bun is the answer when startup speed matters most — scripts running frequently in development, test suites that spawn many processes, or new services where you want the fastest possible development loop and you have verified your dependency tree works on Bun. Starting new projects on Bun is low risk if you test package compatibility upfront.
ts-node should only appear in existing projects that already use it and where the migration cost does not justify the speed improvement. For any new project or script, tsx is strictly better.
Node.js 22 native TypeScript (--experimental-strip-types) is worth knowing about for simple utility scripts with no advanced TypeScript features. Not production-ready as a tsx replacement.
| Scenario | Tool |
|---|---|
| Node.js project, new scripts | tsx |
| Fastest possible startup | Bun |
| New project, Bun runtime | Bun |
| Existing ts-node project | Keep ts-node or migrate to tsx |
| Simple script, Node.js 22+ | --experimental-strip-types |
| Production server (Node.js) | Compile with tsup/tsc, run JS |
| Production server (Bun) | bun run directly |
| CLI tool development | tsx or Bun |
The type checking strategy is the same regardless of which runner you choose: tsx and Bun both skip type checking for speed. Run tsc --noEmit in CI as a separate step, not as part of the script execution path. This is the right separation — fast execution for tight feedback loops, type safety enforced before merging.