Skip to main content

Guide

tsx vs ts-node vs esno: TypeScript in Node.js 2026

Compare tsx, ts-node, and esno for running TypeScript without a build step in 2026. Performance, ESM support, tsconfig compatibility, and which to use.

·PkgPulse Team·
0

TL;DR

tsx (TypeScript Execute) is the modern winner in 2026 — powered by esbuild, starts in milliseconds, full ESM support, replaces node drop-in, and works with node --import tsx for seamless Node.js integration. ts-node is the original TypeScript runner — slower (uses TypeScript compiler, not esbuild), but has the most complete TypeScript feature support including paths aliasing, decorators, and experimental features. esno is tsx under the hood (same esbuild-based approach, by the same author). In 2026: use tsx for development scripts and tooling; use a full build step (tsc, tsup, Vite) for production.

Key Takeaways

  • tsx: ~8M weekly downloads — esbuild-powered, instant startup, ESM + CJS, node --import tsx syntax
  • ts-node: ~15M weekly downloads — TypeScript compiler, slower but full TS feature parity
  • esno: ~1M weekly downloads — thin wrapper around tsx (same engine), being superseded by tsx
  • tsx transpiles with esbuild, skipping type-checking — 100x faster than ts-node
  • ts-node is the only option for full TypeScript diagnostics at runtime (via typeCheck: true)
  • Node.js 22.6+ has experimental native TypeScript stripping — the future is built-in

The Problem: TypeScript in Scripts and Dev Tools

// Problem: Node.js doesn't natively run .ts files
// node src/migrate.ts → SyntaxError: Unexpected token ':'

// Solution: a TypeScript runner transpiles on the fly:
// tsx src/migrate.ts    → runs instantly (esbuild)
// ts-node src/migrate.ts → runs (TypeScript compiler, slower)

// Use cases:
//   - Database migration scripts
//   - CLI tools
//   - Development scripts (seeding, fixtures)
//   - Testing (vitest, jest with ts-jest)
//   - Build scripts that aren't worth compiling
//   - Rapid prototyping

tsx

tsx — esbuild-powered TypeScript runner:

Basic usage

# Install:
npm install -D tsx

# Run a TypeScript file:
npx tsx src/index.ts
npx tsx src/scripts/migrate.ts

# Pass arguments:
npx tsx src/scripts/seed.ts --env staging

# Watch mode (re-run on file changes):
npx tsx watch src/index.ts

Drop-in replacement for node

# Use tsx as a node replacement:
tsx src/index.ts

# Works with node flags:
tsx --env-file=.env src/index.ts
node --import tsx src/index.ts   # Preferred for Node.js 18+

package.json scripts

{
  "scripts": {
    "dev": "tsx watch src/index.ts",
    "migrate": "tsx src/db/migrate.ts",
    "seed": "tsx src/db/seed.ts",
    "generate": "tsx scripts/generate-types.ts"
  },
  "devDependencies": {
    "tsx": "^4.0.0"
  }
}
# Register tsx as a loader — allows require/import of .ts files:
node --import tsx/esm src/index.ts         # ESM mode
node --require tsx/cjs src/index.ts        # CJS mode

# In package.json:
{
  "scripts": {
    "start:dev": "node --import tsx src/index.ts"
  }
}

With Vitest and Jest

// vitest.config.ts — tsx handles TypeScript for vitest natively:
import { defineConfig } from "vitest/config"

export default defineConfig({
  test: {
    // Vitest already uses Vite/esbuild — no tsx needed for tests
  },
})

// jest.config.ts — use tsx as transformer:
export default {
  transform: {
    "^.+\\.tsx?$": [
      "ts-jest",
      { useESM: true },
    ],
    // Or use tsx with jest directly via @jest/globals
  },
}

tsconfig compatibility

// tsx reads your tsconfig.json automatically:
// compilerOptions.paths → resolved (via esbuild's tsconfig-paths)
// compilerOptions.target → respected
// compilerOptions.moduleResolution → bundler mode
// decorators → supported via esbuild

// tsx does NOT run type checking (it's a transpiler, not type checker)
// Run type checking separately:
// tsc --noEmit

What tsx does NOT support

# tsx strips types — it does NOT:
#   - Report type errors
#   - Support experimental features not in esbuild (const enums across files)
#   - Support all tsconfig.json paths configurations equally

# For production with full tsc features, use tsc or tsup to build:
npx tsc --noEmit    # Type check only (no output)
npx tsup src/index.ts  # Build with full TS + bundle

ts-node

ts-node — the original TypeScript Node.js runner:

Basic usage

# Install:
npm install -D ts-node typescript

# Run:
npx ts-node src/index.ts

# REPL:
npx ts-node

tsconfig.json for ts-node

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "CommonJS",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "paths": {
      "@/*": ["./src/*"]
    }
  },
  "ts-node": {
    "swc": true,          // Use SWC for transpilation (much faster!)
    "esm": false,         // Enable ESM (requires Node.js 12+)
    "experimentalSpecifierResolution": "node"
  }
}

SWC mode (fast ts-node)

# ts-node with SWC transpiler (similar speed to tsx):
npm install -D ts-node @swc/core @swc/helpers

# In tsconfig.json:
# "ts-node": { "swc": true }

npx ts-node --swc src/index.ts
# Or with the config above, just:
npx ts-node src/index.ts

ESM mode

# ts-node ESM mode requires:
# 1. tsconfig.json: { "compilerOptions": { "module": "ESNext" } }
# 2. package.json: { "type": "module" }

npx ts-node --esm src/index.ts

# Or use node with loader:
node --loader ts-node/esm src/index.ts

Full type checking at runtime

// ts-node with type checking enabled (slow but catches type errors):
// tsconfig.json:
{
  "ts-node": {
    "typeCheck": true   // Default: false. Enables full diagnostics.
  }
}

// CLI:
// ts-node --type-check src/index.ts

// Use case: CI validation scripts where type errors should halt execution
// Not recommended for dev iteration — too slow

ts-node vs tsx performance

Cold start (simple script):
  ts-node (default):    ~2000ms (TypeScript compiler)
  ts-node (swc: true):  ~200ms (SWC transpiler)
  tsx:                  ~100ms (esbuild)
  esno:                 ~100ms (same as tsx)
  Bun (built-in TS):    ~50ms (no tool needed)

For 100-file project:
  ts-node: noticeably slower on each run
  tsx: still fast — esbuild is extremely efficient

esno

esno — thin esbuild-based runner:

# esno is a thin wrapper around tsx with a slightly different CLI:
npm install -D esno

# ESM mode:
npx esno src/index.ts

# CJS mode:
npx esno/esno src/index.ts  # or: npx cjs-esno src/index.ts
// esno is now just an alias for tsx from the same author
// The author (antfu) recommends using tsx directly in new projects:
// "esno is essentially the same as tsx now"

// If you see esno in older projects, it can be replaced with tsx:
// npm uninstall esno && npm install -D tsx
// Replace "esno" with "tsx" in package.json scripts

Feature Comparison

Featuretsxts-nodeesno
TranspileresbuildTypeScript (or SWC)esbuild
Cold start⚡ ~100ms🐌 ~2000ms (or ~200ms with SWC)⚡ ~100ms
Type checking✅ (optional)
ESM support✅ Full✅ (with config)✅ Full
CJS support
Watch mode
tsconfig paths
Decorators✅ (esbuild)✅ (TS compiler)
const enum across files
Bun alternativebun runN/AN/A
Weekly downloads~8M~15M~1M

When to Use Each

Choose tsx if:

  • Running TypeScript scripts, CLIs, and dev tooling (the default choice in 2026)
  • Need fast startup for scripts that run frequently
  • Full ESM support with node --import tsx
  • Drizzle migrations, Prisma seed files, custom build scripts

Choose ts-node if:

  • You need runtime type checking (typeCheck: true)
  • Using TypeScript features esbuild doesn't fully support (complex const enum across files)
  • Legacy project already on ts-node — no reason to migrate unless performance is a problem
  • Need maximum tsconfig.json compatibility

Choose esno if:

  • Inherited a project using esno — it works fine, consider migrating to tsx

Use Bun instead if:

  • Already running Bun — it runs TypeScript natively with zero configuration (bun run src/index.ts)

For production builds — don't use any of these:

# Always build for production with a real compiler:
npx tsc --outDir dist       # Full TypeScript compiler
npx tsup src/index.ts       # esbuild with TypeScript, bundles + dts
npx tsdown src/index.ts     # Rolldown-based, faster tsup alternative

Drizzle Migrations, Seed Scripts, and the Real tsx Workflow

The most common real-world use of tsx in 2026 is running database tooling without a build step. Drizzle ORM's migration runner, Prisma's seed files, and custom fixture generators are all TypeScript files that need to execute in a Node.js environment with access to environment variables, database connections, and file system paths. Before tsx, teams had two bad options: compile these scripts to JavaScript first (adding friction to a task that should be trivial) or use ts-node with a separate tsconfig that set "module": "CommonJS" to avoid ESM headaches.

tsx eliminates both problems. A typical package.json setup puts "migrate": "tsx src/db/migrate.ts" and "seed": "tsx src/db/seed.ts" in the scripts block with no additional configuration. The script picks up tsconfig.json automatically, resolves path aliases like @/lib/db via esbuild's tsconfig-paths support, and starts in under 200ms. For scripts that run in CI — fixture generators, code generation, report exporters — tsx's consistent cold-start performance is a practical advantage over ts-node's variable startup time depending on project size.

One friction point: tsx does not type-check. If a seed script has a type error that would normally be caught by tsc, tsx runs it anyway and the error only surfaces at runtime. The standard solution is to keep tsc --noEmit running in a pre-commit hook or CI step separately, treating tsx as a fast execution layer and TypeScript's compiler as a separate validation pass.

ts-node's SWC Mode and When It Still Wins

ts-node's reputation for slow startup is largely an artifact of its default configuration, where it invokes the TypeScript compiler directly. With "swc": true set in tsconfig.json's ts-node section, ts-node delegates transpilation to SWC — a Rust-based compiler that brings startup time down to roughly 150-200ms, competitive with tsx. This matters for teams that have existing tooling built around ts-node (custom REPL scripts, legacy Jest configurations using ts-jest, monorepo setups that rely on ts-node's --project flag to target specific tsconfigs) and don't want to migrate but do want better performance.

The case where ts-node with SWC still loses to tsx is const enum declarations spread across multiple files. TypeScript's const enum is inlined at compile time — the compiler substitutes the numeric value directly into calling code. esbuild (and therefore tsx) processes each file independently and cannot perform this cross-file substitution, so const enum values from external modules appear as undefined at runtime. ts-node uses the TypeScript compiler's full program context even in SWC mode, so it handles const enum correctly. This is a narrow edge case — most codebases can replace const enum with enum or plain object constants — but it's the one scenario where ts-node is technically more correct.

Node.js Native TypeScript Stripping: The 2026 Context

Node.js 22.6 introduced experimental support for running TypeScript files directly via --experimental-strip-types, and Node.js 23.x promoted it closer to stable. This feature uses a lightweight type-stripping approach — it removes TypeScript syntax without performing any compilation or transformation, leaving the rest of the code as valid JavaScript. The implications for tsx and ts-node are significant: for simple scripts with no TypeScript-only syntax beyond type annotations (no decorators, no const enum, no tsconfig.json path aliases), node --experimental-strip-types script.ts works with zero npm dependencies.

In practice, tsx remains the better developer experience in 2026 because it handles the full TypeScript feature set, resolves path aliases, and requires no Node.js version lock-in. But the trajectory is clear: the gap between "install tsx" and "built into node" is shrinking. Projects that target Node.js 23+ can already skip tsx for simple utility scripts, and by 2027 the experimental flag is expected to be removed. For now, tsx is the pragmatic default because it works identically across Node.js 18, 20, 22, and 23 without any flags.


Methodology

Download data from npm registry (weekly average, February 2026). Feature comparison based on tsx v4.x, ts-node v10.x, and esno v0.x.

Compare TypeScript tooling and developer experience packages on PkgPulse →

The watch mode behavior of tsx is another practical advantage for development scripts. tsx watch src/server.ts restarts the process when any imported file changes, using Node.js's built-in file watching. Unlike nodemon (which watches by file extension or glob patterns), tsx's watch mode understands the actual import graph — it only restarts when a file that the running script actually imports is modified, not when any .ts file in the project changes. This precision reduces unnecessary restarts in monorepos where multiple packages share a src/ directory structure. For comparison, ts-node's watch mode is provided by a separate ts-node-dev package (which wraps nodemon) rather than being built-in, adding another dependency. The integrated watch mode is one reason tsx has largely displaced ts-node for development server scripts even on teams that continue using ts-node for other purposes.

A tsx behavior worth knowing before using it in CI pipelines: tsx exits with code 0 even if the executed TypeScript file would have type errors, because it strips types rather than checking them. This is identical to esbuild's behavior and is correct for a transpiler — but it means tsx src/migrate.ts && echo success in a CI step will succeed even if migrate.ts has incorrect types that would prevent it from running correctly. The fix is to run tsc --noEmit src/migrate.ts as a separate CI step before the tsx execution step. This separation is actually cleaner than ts-node's default because it makes the two concerns explicit: type correctness (tsc) and execution (tsx). A common pattern is to run type checking as a pre-step in the CI matrix and tsx execution as the actual job step, relying on CI job dependencies to enforce ordering.

See also: cac vs meow vs arg 2026 and cosmiconfig vs lilconfig vs conf, archiver vs adm-zip vs JSZip (2026).

The 2026 JavaScript Stack Cheatsheet

One PDF: the best package for every category (ORMs, bundlers, auth, testing, state management). Used by 500+ devs. Free, updated monthly.