Running TypeScript: tsx, ts-node & Native
Node.js cannot execute a .ts file on its own — the types have to be removed first. For years that meant compiling with tsc before running, but during development that round-trip is slow and clumsy. Today you have three good options for running TypeScript directly: the long-standing ts-node, the much faster tsx, and Node’s own native type stripping introduced in Node 22. This page compares them and explains when to run TypeScript directly versus compiling ahead of time for production.
How “running” TypeScript actually works
TypeScript is JavaScript with type annotations. To run it, those annotations must be erased (and any TypeScript-only syntax transformed) into plain JavaScript that the V8 engine understands. There are two broad strategies for this:
- Type stripping — just delete the type annotations and leave everything else alone. It is extremely fast and requires no full compile, but it cannot handle constructs that emit real runtime code (like
enumornamespace). - Full transpilation — actually compile the source, optionally type-checking it, and produce JavaScript. Slower, but complete.
The tools below sit at different points on this spectrum, trading speed for completeness and type safety.
ts-node: the original runner
ts-node registers a loader that compiles each TypeScript file on the fly using the real TypeScript compiler, so it understands every language feature and can type-check as it runs. That correctness comes at a cost: startup is noticeably slow, and it needs careful configuration to work smoothly with ES modules.
npm install --save-dev ts-node typescript
// src/index.ts
import { hostname } from "node:os";
const host: string = hostname();
console.log(`Running on ${host}`);
Run it directly. With ESM you invoke the ESM loader:
node --loader ts-node/esm src/index.ts
Output:
Running on my-laptop
By default ts-node type-checks your code, which is part of why it is slow. You can skip checking with --transpile-only (or the swc backend) to speed things up, at which point you may as well reach for tsx.
tsx: the fast modern runner
tsx (“TypeScript Execute”) uses esbuild under the hood to strip and transpile TypeScript in milliseconds. It supports both ESM and CommonJS automatically, has first-class watch mode, and needs essentially no configuration. For day-to-day development it has become the default choice.
npm install --save-dev tsx
Run a file directly, or use watch mode for an instant inner loop that restarts on every save:
npx tsx src/index.ts
npx tsx watch src/index.ts
Output:
Running on my-laptop
The important trade-off: tsx does not type-check. It strips and transpiles for speed, trusting that your editor and a separate tsc --noEmit step catch type errors. In practice this is the right division of labour — run fast, check separately.
Run type checking as its own command —
tsc --noEmit— in CI and as a pre-commit hook.tsxand native stripping happily run code that has type errors, so without a dedicated check those errors reach production unnoticed.
Native type stripping in Node
Starting with Node 22.6 you can run TypeScript with no extra dependency at all using --experimental-strip-types. Node erases the type annotations and runs the result. From Node 22.7 the flag is unflagged for stripping and --experimental-transform-types adds support for enum and namespace. In Node 23.6+ and 24, stripping is on by default — node file.ts just works.
# Node 22.6 – 23.5
node --experimental-strip-types src/index.ts
# Node 23.6+ / 24 (enabled by default)
node src/index.ts
Output:
Running on my-laptop
Because it only strips types, the native mode rejects TypeScript-specific runtime constructs unless you also pass --experimental-transform-types. Annotation-only code — interfaces, type aliases, as casts, parameter and return types — works out of the box.
Comparison
| Tool | Type-checks? | Speed | Extra dependency | ESM + CJS | Best for |
|---|---|---|---|---|---|
ts-node | Yes (default) | Slow | Yes | Needs config | Correctness-first, legacy setups |
tsx | No | Very fast | Yes | Automatic | Everyday development |
| Native strip (Node 22+) | No | Fast | None | ESM + CJS | Zero-dependency dev, scripts |
tsc then node | Yes (at build) | Build-time | Yes (dev only) | Per config | Production builds |
Dev versus production
Running TypeScript directly is a development convenience. In production you almost always want to compile ahead of time with tsc and ship the emitted JavaScript:
- Startup is faster because there is no per-file transpilation at boot.
- The runtime image stays slim — no
tsx,ts-node, or@types/*shipped. - A real
tscbuild is the most portable artifact and runs on any Node version.
A typical package.json separates the two concerns cleanly:
{
"scripts": {
"dev": "tsx watch src/index.ts",
"build": "tsc",
"start": "node dist/index.js",
"typecheck": "tsc --noEmit"
}
}
Native stripping blurs this line a little — you can run .ts directly in production on Node 22+ — but a precompiled build remains the recommended default for serious services.
Best Practices
- Use
tsx(or native stripping) for the dev loop; it gives the fastest feedback with zero or minimal setup. - Never rely on the runner for type safety — add a dedicated
tsc --noEmitcheck in CI and pre-commit. - Prefer native stripping for quick scripts and tooling to avoid adding dependencies entirely.
- Avoid
enumandnamespaceif you want code to run under plain type stripping without extra flags. - For production, compile with
tscand run the emitteddist/JavaScript with plainnode. - Pin
@types/nodeto your runtime’s major version so types match the APIs you actually run against.