tsx vs ts-node vs Bun: Running TypeScript Directly 2026
·PkgPulse Team
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) and ESM support is historically painful. Bun is the fastest option overall — TS is native, no configuration needed — but Node.js compatibility issues remain for production servers. For scripts and CLIs: tsx. For production servers: tsx (for Node.js) or Bun (if compatibility is confirmed). ts-node: only for legacy projects.
Key Takeaways
- tsx: ~2M downloads/week, esbuild-based, ~50ms startup, full ESM + CJS, Node.js 18+ native
- ts-node: ~7M downloads/week but declining, tsc-based, ~500ms startup, ESM pain
- Bun: ~900K downloads/week, native TypeScript runtime, ~10ms startup, 90%+ Node.js compat
- Startup time: Bun (10ms) > tsx (50ms) > ts-node (500ms)
- Node.js compat: ts-node = full, tsx = full, Bun = 90%+
- For development: tsx or Bun (your choice); avoid ts-node in new projects
Downloads
| Package | Weekly Downloads | Trend |
|---|---|---|
ts-node | ~7M | ↓ Declining |
tsx | ~2M | ↑ Fast growing |
bun | ~900K | ↑ Growing |
tsx: The Node.js Standard
npm install --save-dev tsx
# Run TypeScript file:
npx tsx src/index.ts
# Watch mode (re-runs on changes):
npx tsx watch src/index.ts
# Pass Node.js flags:
node --import tsx/esm src/index.ts # Node.js 22+ loader mode
tsx Configuration
// package.json:
{
"scripts": {
"dev": "tsx watch src/server.ts",
"start": "tsx src/server.ts",
"script": "tsx scripts/seed.ts"
},
// For ESM projects:
"type": "module"
}
// Works with .ts, .tsx, .cts, .mts files:
// src/server.ts — Express example:
import express from 'express';
import { db } from './lib/db.js'; // Note: .js extension (ESM standard)
const app = express();
app.get('/health', (_, res) => {
res.json({ status: 'ok', timestamp: new Date().toISOString() });
});
app.listen(3000, () => console.log('Server running on :3000'));
tsx Speed Benchmark
Script startup time (simple TypeScript file):
tsx: ~48ms
ts-node: ~480ms ← 10x slower (runs full tsc)
Bun: ~12ms
Implication for scripts that run 100x/day:
tsx: 4,800ms total startup overhead
ts-node: 48,000ms total startup overhead
ts-node: The Legacy Standard
npm install --save-dev ts-node typescript @types/node
# Run:
npx ts-node src/index.ts
# ESM (painful — requires extra setup):
npx ts-node --esm src/index.ts
// tsconfig.json additions needed for ts-node:
{
"compilerOptions": {
"module": "commonjs", // Or "nodenext" for ESM (more config needed)
"esModuleInterop": true,
"resolveJsonModule": true
},
"ts-node": {
"esm": true, // Enable ESM
"experimentalSpecifierResolution": "node"
}
}
Why ts-node is declining:
- Requires TypeScript as a peer dependency (tsx doesn't — uses esbuild)
- ESM support requires extra tsconfig options
- 10x slower startup vs tsx
- ts-node/esm loader is deprecated in favor of tsx
Bun: Native TypeScript Runtime
# Install Bun:
curl -fsSL https://bun.sh/install | bash
# Run TypeScript directly (no tsx/ts-node needed):
bun run src/server.ts
bun run src/script.ts
# Bun watch mode:
bun --watch run src/server.ts
# Run npm scripts:
bun dev
bun test # Bun's built-in test runner
Bun: No Configuration
// src/server.ts — Bun's native HTTP server:
const server = Bun.serve({
port: 3000,
fetch(req) {
const url = new URL(req.url);
if (url.pathname === '/health') {
return Response.json({ status: 'ok' });
}
return new Response('Not Found', { status: 404 });
},
});
console.log(`Server running at ${server.url}`);
// Or use Express/Hono on Bun (mostly compatible):
import { Hono } from 'hono';
const app = new Hono();
app.get('/health', (c) => c.json({ status: 'ok' }));
export default {
port: 3000,
fetch: app.fetch,
};
Bun Compatibility Notes
✅ Works on Bun:
- Most npm packages (fetch, crypto, path, fs, etc.)
- Express, Hono, Fastify (with minor notes)
- Prisma ORM
- Drizzle ORM
- Most Zod operations
- Most AWS SDK calls
⚠️ Partial/needs testing:
- Node.js native modules (gyp-compiled)
- Some legacy packages using old Node internals
- Worker threads (Bun workers ≠ Node workers)
- Certain spawn/child_process behaviors
❌ Doesn't work on Bun:
- Some packages that deep-depend on exact Node.js internals
- Anything requiring specific Node.js version APIs not in Bun
Comparison Table
| tsx | ts-node | Bun | |
|---|---|---|---|
| Startup time | ~50ms | ~500ms | ~10ms |
| TypeScript execution | esbuild (transpile only) | tsc (type check) | Native |
| Type checking | ❌ (separate tsc) | ✅ | ❌ (separate tsc) |
| ESM support | ✅ Full | ⚠️ Painful | ✅ Full |
| Node.js compat | 100% | 100% | ~90-95% |
| Watch mode | ✅ tsx watch | ✅ | ✅ bun --watch |
| No config needed | ✅ | ❌ | ✅ |
| npm packages | All | All | ~95% |
| Production servers | ✅ | ✅ | ✅ (verify compat) |
Decision Guide
Use tsx if:
→ Node.js is your runtime
→ Running TypeScript scripts, seeds, migrations
→ Building a CLI tool
→ Development server that needs 100% Node.js compat
→ Default choice for new Node.js TS projects
Use Bun if:
→ Maximum speed (scripts, CI, development)
→ Starting a new project happy on Bun runtime
→ Simple servers or API services
→ Comfortable testing package compatibility
Use ts-node if:
→ Existing project already using it
→ Need actual TypeScript type checking during execution
→ Can't migrate legacy configuration
Type checking strategy:
→ tsx/Bun don't type-check (they only transpile)
→ Run `tsc --noEmit` separately in CI or as a parallel script
→ Or use: "typecheck": "tsc --noEmit"
Compare tsx, ts-node, and Bun package health on PkgPulse.