Pair this with a strict TypeScript config (see TypeScript strict mode), --experimental-strip-types removes the runtime type check, so a strict editor and CI pass become the only thing standing between you and silently-wrong code.
For years, running TypeScript in Node.js meant choosing a tool: ts-node, tsx, Bun, Deno. Node.js 23.6 changed that with native TypeScript support. The --experimental-strip-types flag means the runtime can run .ts files directly by stripping type annotations before execution. No transpilation, no sourcemaps, no extra config, just node.
I've been using this on CLI scripts and internal tooling for a few months now. The startup time difference alone is worth understanding.
Quick take: Native TypeScript execution is Node.js 23.6's ability to run .ts files directly via --experimental-strip-types, no ts-node, no tsx, no build step. It strips type annotations using @swc/wasm-typescript, so errors go uncaught unless tsc --noEmit runs separately. Startup drops to about 15ms, versus 120ms for tsx and 300ms for ts-node. Enums and decorators still need a real build.
How Does --experimental-strip-types Actually Work?
Type stripping is the process of removing TypeScript-only syntax, annotations, interfaces, generics, from a file before execution, without converting the remaining code into a different target syntax the way a transpiler does. Type stripping is not transpilation. Node.js 23.6 uses the @swc/wasm-typescript package internally to parse TypeScript files and remove type annotations before handing the code to V8, according to the Node.js v23.6.0 Release Notes. The JavaScript that V8 sees is structurally identical to the TypeScript you wrote, minus all the type syntax.
What that means in practice: no ES feature downleveling, no decorator transforms, no type checking. If you write async/await, it stays async/await. Node.js executes the JavaScript that remains after stripping, using whatever V8 version ships with that Node.js release.
What TypeScript Syntax Is Supported
Everything that's purely a type annotation works fine. That includes:
- Type annotations on variables, function parameters, and return types (
const name: string = "hello") - Interface declarations and type aliases (
interface User { id: number }) - Generic type parameters (
function identity<T>(val: T): T) astype assertions and non-null assertions (value!)
Here's the simplest example. Save this as server.ts:
const port: number = 3000;
function greet(name: string): string {
return `Hello, ${name}`;
}
console.log(greet("world"));
console.log(`Listening on port ${port}`);
Run it with:
node --experimental-strip-types server.ts
That's it. No config, no install.
What TypeScript Syntax Is NOT Supported
Enums are the main casualty. They generate real JavaScript, a lookup object, not just syntax. Same with namespaces: they compile to IIFE wrappers. Decorators (the kind NestJS and TypeORM rely on) aren't supported either.
// This will fail with --experimental-strip-types
enum Direction {
Up,
Down,
Left,
Right,
}
Replace enums with union types instead. See TypeScript patterns for the full argument, but the short version is that union types are more flexible and have no runtime overhead.
When Does This Replace ts-node and tsx?
Startup overhead refers to the fixed cost a tool adds before your actual script logic begins running, parsing, transpiling, or spawning a child process, separate from the time your code itself takes to execute. Before I tried native stripping, I had --import=tsx in every Node.js script in our monorepo. That worked, but tsx 4.x adds roughly 80-150ms of startup overhead per invocation. For scripts that run hundreds of times during a build or test suite, that adds up fast.
Native type stripping adds near-zero overhead. The SWC-based stripping is synchronous and runs in the same process, no child process, no separate compiler pass.
The sweet spot for the native flag is:
- One-off scripts, database migrations, data seeding, code generation tools
- CLI utilities, internal tools you run from the command line
- Development tooling, anything you're running locally that doesn't ship to production
- Monorepo scripts, those
scripts/folders that were the whole reason you reached for tsx
Here's the before-and-after for a typical monorepo script:
// package.json, old way
"scripts": {
"seed": "tsx scripts/seed-database.ts",
"migrate": "tsx scripts/run-migrations.ts"
}
// package.json, new way (Node.js 23.6+)
"scripts": {
"seed": "node --experimental-strip-types scripts/seed-database.ts",
"migrate": "node --experimental-strip-types scripts/run-migrations.ts"
}
One fewer dependency. Same result.
If you're rewriting CLI tooling for the new runtime, our TypeScript clean code patterns collects the 15 patterns that age well in long-lived scripts, the kind of code that lives in scripts/ for years.
When Do You Still Need a Build Step?
Native type stripping is not a replacement for tsc, and treating it like one is the most common mistake teams make when they first adopt it. It's a convenience for running scripts, not a type checker, and it skips the compiler entirely rather than running a lightweight version of it. Four situations still require a full build step or a separate tsc --noEmit pass in CI, and none of them are edge cases, most teams hit at least two of them within the first month.
Production applications. Type stripping skips type checking entirely. Every TypeScript error you'd normally catch with tsc --noEmit is invisible to the runtime. That means your CI pipeline must run tsc --noEmit as a separate step, it doesn't happen automatically just because Node.js can execute the file.
Decorator-heavy frameworks. NestJS and TypeORM both rely heavily on legacy decorators. The decorator transform hasn't been implemented in the strip-types path. Until that changes, these frameworks need ts-node or a build step with full tsc compilation.
Node.js 18 and 20 LTS environments. The flag exists only in Node.js 22.6+ and is more stable in 23.6+. If you're on an LTS version (which most production servers are), tsx 4.x is still your best option for script execution.
ESM/CJS interop edge cases. The strip-types path has some quirks around module resolution when mixing .ts files with CommonJS dependencies. Most projects won't hit these, but they exist.
The practical takeaway: use native stripping for scripts and local tooling. Use a build step for anything that ships.
How Do You Set Up a Zero-Config TypeScript Project?
The setup is genuinely minimal, and it stays that way whether you're bootstrapping a throwaway CLI tool or a long-lived internal service. You need exactly two files: a package.json with the right scripts, and a tsconfig.json that exists purely for the editor and for tsc --noEmit in CI. Follow these three steps to get a zero-config project running:
- Add
"type": "module"to package.json so Node.js treats.tsfiles as ES modules. - Point your start script at
node --experimental-strip-types src/index.ts(or drop the flag entirely on Node.js 24.3.0+). - Add a minimal tsconfig.json with
strict: trueandnoEmit: trueso your editor and CI still catch type errors that the runtime ignores.
The package.json scripts
{
"name": "my-ts-project",
"type": "module",
"scripts": {
"start": "node --experimental-strip-types src/index.ts",
"typecheck": "tsc --noEmit",
"dev": "node --watch --experimental-strip-types src/index.ts"
}
}
The --watch flag pairs nicely with --experimental-strip-types. You get file-watching restarts without nodemon, without ts-node-dev, without any of the old toolchain.
The minimal tsconfig.json
You still want tsconfig.json. Your editor needs it. Your CI type check needs it.
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"strict": true,
"noEmit": true,
"skipLibCheck": true
},
"include": ["src/**/*.ts"]
}
Set noEmit: true to make it clear this tsconfig is for checking only, not building. The actual execution goes through Node.js's strip-types path. See the strict TypeScript config article for what each strict flag actually enables.
The startup difference is hard to ignore: ts-node 10.x adds roughly 300ms before your code runs, tsx 4.x trims that to about 120ms, and Node native strip-types starts in around 15ms (community benchmarks, Node.js 23.6). For short-lived scripts, that overhead is most of the runtime.
Is Type Stripping Stable Yet in Node.js 24 LTS?
Node.js 24 shipped as the current LTS in 2026, and type stripping followed the roadmap this article predicted, then went further. The --experimental- warning was removed in Node.js 24.3.0, and the feature reached stable status in 24.12.0. The bigger change: on Node.js 24, running node file.ts strips types by default, no flag required at all. The --experimental-strip-types flag from the 23.6 era still works but is no longer necessary.
That makes Node.js 24 the practical baseline for greenfield projects that want to skip a build step for scripts and tooling. If you're still on Node.js 22 or 23, the flag-based syntax in this article is exactly what you need; on 24, drop the flag entirely.
The Node.js team's stated longer-term goal is type checking at the language level, not just stripping, but the current stable implementation still deliberately skips running the TypeScript compiler for performance reasons, so tsc --noEmit in CI remains mandatory either way.
Bun was the main reason developers started expecting "just run TypeScript" as a feature. Deno made the same argument. Node.js closed that gap faster than most expected, and ts-node is now legacy for any new project on Node.js 23+. tsx's advantage was always startup speed over ts-node; now that stripping is stable and flag-free on LTS, tsx's long-term role narrows to a compatibility shim for teams still on Node.js 18 or 20.
How Much Faster Is Native Stripping Than ts-node or tsx?
Startup time is where native stripping earns its keep, and the gap is large enough to matter even in a single CI run. According to community benchmarks run against Node.js 23.6, ts-node 10.x adds roughly 300ms of startup overhead before your first line of code executes, tsx 4.x trims that to about 120ms, and native --experimental-strip-types starts in around 15ms, a twenty-fold improvement over ts-node. For a one-off script that difference barely registers, but for a monorepo test suite that spawns hundreds of short-lived processes, the saved milliseconds add up to real minutes over a full CI run. The table below breaks down the three options side by side across startup cost, type checking behavior, and dependency footprint.
| ts-node 10.x | tsx 4.x | Native --experimental-strip-types | |
|---|---|---|---|
| Startup overhead | ~300ms | ~120ms | ~15ms |
| Type checking | No (runtime) | No (runtime) | No (runtime) |
| Enums/decorators | Supported | Supported | Not supported |
| Extra dependency | Yes | Yes | No, built into Node.js 23.6+ |
What's the Real-World Benefit?
Node.js finally has a path to running TypeScript without extra tooling, and it arrived faster than most of the community expected back when Bun and Deno first made "just run the .ts file" look like a permanent competitive edge. The tradeoff, no type checking at runtime, is fine as long as CI runs tsc --noEmit as a separate, mandatory step. That's not a new constraint, it's what every TypeScript project already does, native stripping just removes the extra dependency that used to sit between your source file and node.
The developer experience improvement is real. One fewer dependency to install, one fewer tool to configure, one fewer reason to reach for Bun or Deno just to run a script. For scripts and internal tooling, node --experimental-strip-types is the right default now.