Skip to main content

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

PackageWeekly DownloadsTrend
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:

  1. Requires TypeScript as a peer dependency (tsx doesn't — uses esbuild)
  2. ESM support requires extra tsconfig options
  3. 10x slower startup vs tsx
  4. 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

tsxts-nodeBun
Startup time~50ms~500ms~10ms
TypeScript executionesbuild (transpile only)tsc (type check)Native
Type checking❌ (separate tsc)❌ (separate tsc)
ESM support✅ Full⚠️ Painful✅ Full
Node.js compat100%100%~90-95%
Watch mode✅ tsx watch✅ bun --watch
No config needed
npm packagesAllAll~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.

Comments

Stay Updated

Get the latest package insights, npm trends, and tooling tips delivered to your inbox.