cheat sheet
tsx
Package-level reference for tsx on npm — enhanced Node.js runtime for TypeScript and ESM, watch mode, ts-node replacement, and Node loader integration.
tsx
What it is
tsx (TypeScript Execute, sometimes called "TS-X") is an enhanced Node.js runtime that runs TypeScript and JSX files directly — no separate compile step, no tsc invocation. Under the hood it uses esbuild to transpile every file on demand and registers itself as a Node module loader so import of .ts, .tsx, .cts, and .mts files Just Works.
It's the de-facto successor to ts-node for new projects: faster startup (esbuild is in Go), seamless ESM support, sensible defaults, and a built-in watch mode. Vite, Astro, and tsup all use tsx-like loaders internally; tsx exposes that capability as a standalone CLI and a Node import hook.
Install
# Project-local (recommended)
npm install -D tsx
pnpm add -D tsx
yarn add -D tsx
bun add -d tsx
Output: tsx binary on PATH under node_modules/.bin/tsx; ready to run tsx, tsx watch, and the tsx/esm / tsx/cjs loader entries.
# Global (rare — prefer per-project)
npm install -g tsx
Output: tsx on the user PATH; updates apply globally but version-skew risk between machines.
# One-off via npx
npx tsx ./script.ts
Output: downloads tsx to the npm cache if missing, then executes the script — useful for ad-hoc scripts outside any project.
Versioning & Node support
- Current line at time of writing is
4.x. The4.xseries stabilised ESM-by-default behaviour and thenode --import tsxregistration model. - Requires Node 18.x or newer. Older Node lacks the
module.register()API thattsx's loader builds on. tsxitself ships as ESM but works inside both ESM and CJS host packages — the loader auto-detects which mode the file expects from the nearestpackage.jsontypefield.- Always a dev dependency. Production should ship pre-built JavaScript;
tsxis fordev, scripts, and one-off invocations. - SemVer respected for the CLI surface; the bundled esbuild minor bumps occasionally without a major.
Package metadata
- Maintainer: Hiroki Osame (
privatenumber) - Project home: github.com/privatenumber/tsx
- Docs: tsx.is
- npm: npmjs.com/package/tsx
- License: MIT
- First released: 2022
- Downloads: multi-million weekly — has overtaken
ts-nodefor new projects
Peer dependencies & extras
tsx has zero peer dependencies. esbuild is bundled — you don't install or upgrade it separately. The only optional partner is typescript itself (for editor type-checking; tsx does not type-check at runtime).
| Companion | Role |
|---|---|
typescript | Type-checking in IDE / pre-commit (tsc --noEmit). tsx strips types, doesn't verify them. |
vitest | Vitest can run TS tests natively, but tsx is handy for ad-hoc test-runner scripts. |
nodemon | Pair nodemon --exec tsx src/server.ts for richer file-watch rules; or just use tsx watch. |
dotenv-cli | dotenv -- tsx ./script.ts loads .env before execution. |
Alternatives
| Tool | Trade-off |
|---|---|
| ts-node | The original. Slower (TypeScript-compiler-based), trickier ESM story, fewer defaults. Maintenance has slowed; new projects prefer tsx. |
| node --import (native) | Node 22.6+ ships an experimental --experimental-strip-types flag — flat types stripping, no transformation. Lightweight but no JSX, no decorators, no paths. |
| bun | Bun executes TypeScript natively — fastest of all options, but commits you to the Bun runtime (different from Node). |
| deno | Same logic — Deno runs TS natively, but it's a separate runtime with its own permissions model and import URLs. |
| swc-node | SWC-based loader; comparable speed to tsx. Less polished CLI surface. |
| esbuild-runner | Lower-level esbuild loader. Smaller, but you wire watch / ESM / source maps yourself. |
Real-world recipes
Worked examples for the most common ways tsx shows up in a project.
Run a single TypeScript file
# Execute and exit
tsx src/script.ts
# Pass args
tsx src/script.ts --flag --name=alice
Output:
Hello from script.ts (received --flag, --name=alice)
tsx resolves the file, transpiles it with esbuild, and runs it through Node. Type errors do NOT block execution — that's a feature, not a bug. Run tsc --noEmit separately for verification.
Watch mode for a dev server
# Restart on any imported-file change
tsx watch src/server.ts
# Clear screen between runs
tsx watch --clear-screen src/server.ts
# Ignore extra directories
tsx watch --ignore "fixtures/**" src/server.ts
Output:
[tsx] Starting watch mode for src/server.ts...
Server listening on http://localhost:3000
[tsx] File changed: src/server.ts
[tsx] Restarting...
Server listening on http://localhost:3000
tsx watch traces the import graph from the entry — only files actually imported trigger restarts. Compare with nodemon, which watches by glob and restarts on unrelated changes.
ESM script with top-level await
// src/migrate.ts
import { readFile } from "node:fs/promises";
import { Client } from "pg";
const config = JSON.parse(await readFile("./config.json", "utf8"));
const db = new Client(config);
await db.connect();
const { rows } = await db.query("SELECT version()");
console.log("Postgres:", rows[0].version);
await db.end();
tsx src/migrate.ts
Output:
Postgres: PostgreSQL 16.2 on x86_64-pc-linux-gnu
tsx always runs in ESM mode for .ts / .mts files — top-level await, import.meta.url, and dynamic import() all Just Work without "type": "module" ceremony.
Vitest companion for one-off integration scripts
{
"scripts": {
"test": "vitest run",
"test:integration": "tsx ./scripts/integration-suite.ts",
"seed": "tsx ./scripts/seed-db.ts"
}
}
tsx is the cheap way to keep "real" Node scripts (seeding, migrations, one-off probes) co-located with your test suite. Vitest is for unit tests; tsx covers the long-tail of "run this TS file and exit".
Replace ts-node in package.json scripts
- "dev": "ts-node-esm src/server.ts",
- "migrate": "ts-node --transpile-only ./scripts/migrate.ts",
+ "dev": "tsx watch src/server.ts",
+ "migrate": "tsx ./scripts/migrate.ts",
Removed: ts-node, typescript (if only used at runtime), --esm / --transpile-only flags. The tsconfig.json paths aliases work in tsx if you add the tsconfig-paths package — see CLI doc for the loader-args trick.
Use as a Node --import loader (manual registration)
node --import tsx ./src/server.ts
Output:
Server listening on http://localhost:3000
Useful when you want full control over Node flags (--max-old-space-size, --inspect, --enable-source-maps) and don't want tsx's wrapper logic. From Node 20+ this is the supported way to hook a TS loader into a Node process you're already configuring.
Production deployment
tsx is not for production. Always build to JavaScript and ship the JS — tsx re-transpiles on every cold start, which is fine for dev but burns CPU at scale.
# Build step (recommended)
npx tsc # type-check + emit .js (slowest)
# or
npx tsup src/index.ts --format esm,cjs --dts # bundle + types
# Ship dist/
node dist/index.js
Output:
Server listening on http://localhost:3000
The exception: short-lived scripts, cron jobs, CI tasks where the startup cost is amortised over a long run, and the convenience of skipping a build step is worth it. For long-running servers, build first.
If you must run tsx in prod (compatibility with PM2 / Procfile not under your control), use the native node --import tsx ./dist/server.js form so source-map debugging works.
Performance tuning
tsx's steady-state perf is "fast enough" — the knobs are mostly about reducing startup latency.
Skip the watch banner
TSX_TSCONFIG_PATH=tsconfig.json tsx --no-tsconfig-paths src/server.ts
Output:
Server listening on http://localhost:3000
tsx reads tsconfig.json from disk on every start. In a monorepo with deep configs, pre-resolve the path or pin it via TSX_TSCONFIG_PATH.
Pre-bundle for repeated invocations
For a script run thousands of times a day (cron, hot loop, CI per-PR), bundle once with esbuild and skip tsx:
npx esbuild src/script.ts --bundle --platform=node --outfile=dist/script.js
node dist/script.js
Output:
✓ Bundled 12 files in 89ms
✓ dist/script.js (94 KiB)
Source map control
tsx emits inline source maps by default — convenient but adds size. Disable for batch scripts that don't need stack traces:
tsx --no-source-maps src/script.ts
Output:
script completed in 142 ms
Memory in long-lived watch processes
tsx watch reuses one Node process per restart. Long-lived watch sessions accumulate transient esbuild caches — restart manually if RSS climbs past ~500 MB on a small app.
Version migration guide
tsx is comparatively young and the API has been stable since 4.0.
| From → To | Highlights |
|---|---|
| 3 → 4 | Switched default ESM detection rules; tsx esm and tsx cjs aliases unified. CLI flags --no-cache and --clear-screen standardised. |
| 3.x stability | Same major; minor releases mostly bumped bundled esbuild. |
| 4.x ongoing | node --import tsx became the canonical loader form; legacy --loader tsx removed in favour of --import. |
Watch the changelog for tsx's eventual 5.0 — likely to follow Node's evolution of the module-loader API (the experimental --experimental-loader flag is being phased out in favour of module.register()).
Security considerations
tsx is a developer tool — its security surface is small but real.
- Arbitrary code execution by design.
tsx ./untrusted.tsruns the file with full Node privileges. Never run untrusted.tsfiles viatsx; sandbox via container,--permission(Node 22+), or a separate user. - Source-map leaks. Inline source maps embed file paths from your workstation. Don't ship
tsx-emitted JS to production — those paths leak through stack traces and dev-tools. tsconfig.jsonextendschains.tsxresolvesextendsrecursively; a malicious package inextendscould control compile flags. Pin your tsconfig dependencies (@tsconfig/node20etc.) the same way you pin runtime deps.- No type checking at runtime.
tsxstrips types, doesn't enforce them. A boundary that relies on type narrowing for security (e.g. "this field is'admin' | 'user'") will accept any string at runtime. Always validate inputs withzod/valibot/ manual checks. - Loader hook collisions. Custom
--importhooks layered on top of tsx must register after tsx, not before — registration order affects whether.tsfiles reach your hook at all.
Testing & CI integration
# .github/workflows/ci.yml
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- run: npm ci
- run: npx tsc --noEmit # type-check (tsx doesn't do this)
- run: npx tsx ./scripts/db-up.ts # start test infra
- run: npm test # vitest / jest
- run: npx tsx ./scripts/db-down.ts # teardown
For monorepos, prefer pnpm exec tsx ./scripts/... so the workspace's pinned tsx version runs (not whatever's on the global $PATH).
Ecosystem integrations
tsx plays nicely with most of the modern Node ecosystem because it's "just a Node loader":
- Vitest — Vitest's own transformer is independent;
tsxis for non-test scripts. Both work side by side. - Vite — uses an embedded esbuild loader internally;
tsxexposes the same capability as a CLI. - Astro — Astro's Node integration uses a tsx-style loader for
.tsmiddleware. - tsup — sibling project by the same maintainer (
privatenumber); pairs naturally for "tsx in dev, tsup for ship". - pkgroll — another
privatenumberlibrary; builds packages whose entry was developed under tsx. - nodemon —
nodemon --exec tsx src/server.tsif you need nodemon's globbing rules overtsx watch's import-graph tracking. - dotenvx / dotenv-cli — wrap as
dotenvx run -- tsx ./script.tsto inject env before transpile.
Troubleshooting common errors
Unknown file extension ".ts"
The Node process is running without the tsx loader. Either invoke via the tsx binary (tsx ./file.ts) or pass --import tsx to your manual node invocation:
node --import tsx ./file.ts
Output:
hello from file.ts
Cannot find module '@/utils' with TS paths
tsx does NOT resolve tsconfig.json paths aliases by default. Add the tsconfig-paths companion or @tsx/aliases loader:
node --import tsx --import tsconfig-paths/register ./src/server.ts
Output:
Server listening on http://localhost:3000
SyntaxError: Cannot use import statement outside a module
The file is being interpreted as CJS but contains ESM syntax. Either rename to .mts, set "type": "module" in the nearest package.json, or use tsx --experimental-specifier-resolution=node.
Watch mode misses changes in monorepo siblings
tsx watch follows the static import graph. Files imported via dynamic require() or string-interpolated import() aren't tracked. Wrap with nodemon --watch ../shared for explicit globbing.
Memory leak after many restarts
esbuild caches accumulate. Kill and re-spawn tsx watch every few hours during long dev sessions, or use nodemon as a supervising parent.
When NOT to use this
- Production servers. Build to JS —
tsxre-transpiles every cold start. - Type-sensitive deploys.
tsxstrips types without checking. Pair with atsc --noEmitpre-flight, or use a build step that does both. - Performance-critical hot paths. The startup overhead (Node + esbuild + loader registration) is ~50–100 ms — fine for scripts, painful in tight loops. Build a binary with
esbuild --bundleor usebundirectly. - Browser code.
tsxis server-side only — it loads code via Node's module system. For browser TS use Vite / esbuild / Webpack. - Projects already committed to bun or deno. Both runtimes execute TypeScript natively; layering
tsxon top buys nothing.
See also
- JavaScript: tsx — CLI commands, flags, common scenarios
- JavaScript: typescript — language features
tsxruns - Packages: npm-ts-node — the predecessor
tsxlargely replaces - Concept: async — top-level
awaitin ESM scripts