Running TypeScript (tsx, ts-node)
There are two ways to get TypeScript running on Node: compile to JavaScript first with tsc and run the output, or run the .ts files directly with a runner that strips or transpiles types on the fly. The compile-then-run path is the right model for production; the direct-run path is what makes local development fast and pleasant. This page compares the leading runners — tsx, ts-node, and Node’s own built-in type stripping — and shows when to reach for each.
Compile then run
The most predictable approach is to build with tsc and execute the emitted JavaScript. What you ship is exactly what you tested, with no runtime transpilation step.
npx tsc # emits dist/ per tsconfig
node dist/index.js # run the compiled output
This is the recommended pattern for production deployments and Docker images. The downside for development is the edit-compile-run cycle, which is why most teams run .ts directly while iterating and only compile for releases.
tsx — the fast default
tsx is a thin wrapper around esbuild that runs TypeScript and ESM with almost zero configuration. It is fast, handles both module systems, and ships a watch mode that restarts on file changes.
npm install --save-dev tsx
npx tsx src/index.ts # run once
npx tsx watch src/index.ts # restart on change
Because tsx uses esbuild, it strips types without type-checking — it never reports type errors, it just transpiles. That makes startup extremely fast, but you must run tsc --noEmit separately (in CI or a pre-commit hook) to catch type errors. Treat tsx as your runner and tsc as your validator.
tsxdoes not type-check. Add a separate"typecheck": "tsc --noEmit"script so type errors are still caught —tsxalone will happily run broken types.
ts-node — the classic runner
ts-node predates tsx and integrates directly with the TypeScript compiler, so it can optionally type-check as it runs. It is still common in older projects and tools like Mocha that hook into it.
npm install --save-dev ts-node typescript
npx ts-node src/index.ts # transpile-only-ish, full compiler
npx ts-node --transpileOnly src/index.ts # skip type-checking for speed
For ESM projects you often need extra flags or the ts-node/esm loader, which is fiddlier than tsx. Its advantage is fidelity: it uses the real tsc API, so behavior matches your build more closely than esbuild-based runners.
Native type stripping
Modern Node can run TypeScript directly by erasing the type annotations itself — no extra dependency. From Node 22.6 this lives behind --experimental-strip-types, and it is enabled by default in Node 23.6 and later.
node --experimental-strip-types src/index.ts # Node 22.6+
node src/index.ts # Node 23.6+ (default on)
Native stripping only removes types; it does not transform TypeScript-specific constructs that emit runtime code, such as enum or namespace, unless you also pass --experimental-transform-types. Like tsx, it does no type-checking. It is ideal for scripts and small services where you want to avoid a build step and an extra tool.
Choosing a runner
| Tool | Type-checks? | ESM ease | Speed | Best for |
|---|---|---|---|---|
tsc + node | Yes (at build) | Native | n/a (precompiled) | Production builds |
tsx | No | Excellent | Very fast | Local dev, scripts |
ts-node | Optional | Needs loader | Moderate | Legacy projects, Mocha |
node --strip-types | No | Native | Fast | Scripts, zero-dep runs |
A practical setup: use tsx watch for development, tsc for the production build, and tsc --noEmit in CI. This keeps the inner loop fast while guaranteeing that nothing ships with type errors.
Wiring it into scripts
Combine the runners into npm scripts so each command has a clear job. Development reloads instantly, while build and typecheck stay separate and explicit.
// package.json
{
"scripts": {
"dev": "tsx watch src/index.ts",
"build": "tsc",
"start": "node dist/index.js",
"typecheck": "tsc --noEmit"
}
}
With this layout, npm run dev gives you hot restarts, npm run typecheck gates merges, and npm run build && npm start mirrors production. The runner you use for speed never replaces the type-check that guarantees correctness.
Best Practices
- Use
tsx watchfor local development — it is fast and needs almost no config. - Always run
tsc --noEmitsomewhere (CI or pre-commit); fast runners skip type-checking. - Compile with
tscfor production rather than transpiling at runtime. - Prefer native
--experimental-strip-typesfor small zero-dependency scripts. - Remember that type stripping does not transform
enum/namespacewithout extra flags. - Reach for
ts-nodemainly when a tool requires it or you need compiler-faithful behavior. - Keep dev runner and build compiler aligned on the same
tsconfig.json.