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.
In the monorepo behind this site, the secret scan in CI runs as node --experimental-strip-types packages/shared/src/security/scan-secrets.ts ., so it needs no yarn install first, while the rest of the scripts still go through --import=tsx. That split is what made me measure the startup difference instead of guessing at it.
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.
Type stripping removes TypeScript-only syntax before execution without transpiling the rest of the file; the JavaScript V8 receives is structurally identical to the TypeScript you wrote, just with annotations, interfaces, and generics erased, using the same @swc/wasm-typescript parser Node.js 23.6 ships internally.
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.
Node.js can run TypeScript directly now, and the ts-node era is quietly over.
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. Most scripts/ folders start life with --import=tsx in front of every script, this monorepo's included. That works. Measured against bare node on the same M1, tsx 4.21 added roughly 100 to 130ms per invocation, which adds up for scripts that run hundreds of times during a build or test suite.
Native type stripping added roughly 25ms over bare node on the same script. The stripping is synchronous and runs in the same process, with no separate compiler pass.
Native strip-types adds only tens of milliseconds over bare Node because it strips synchronously in the same process, while tsx and ts-node add a compiler layer before your script's first line runs, and that cost multiplies across hundreds of runs.
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.
Treating --experimental-strip-types as a replacement for tsc is the most common early mistake: it is a convenience for running scripts, not a type checker, and it skips the compiler entirely rather than running a lighter version of it, which is why CI still needs a separate tsc --noEmit step.
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.
A zero-config native TypeScript project needs exactly two files: a package.json pointing node at your entry file with --experimental-strip-types, and a tsconfig.json with noEmit true that exists purely for editor IntelliSense and a separate tsc --noEmit check in CI, nothing else to install or configure.
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: in my measurements a hello-world script took about 800ms through ts-node 10.9.2, about 160ms through tsx 4.21 and about 55ms through native strip-types, against roughly 30ms for bare node (medians of 15 runs, M1, Node 22.23.2). 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 24.18.0 release notes cover one of the LTS patch releases on that line, and the Node.js 24 features guide covers what the line added overall.
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.
What Changed in Node.js and TypeScript as of October 2026?
Three dates matter this month. Node 24 moves from Active LTS to Maintenance on October 20, Node 26 (Current since May 5) becomes LTS on October 28, and Node 22 keeps security fixes until April 30, 2027, according to the Node.js release schedule. The one change that touches this article is in 26.0.0: --experimental-transform-types is gone. That flag was the escape hatch for enums and parameter properties on 22 through 25.
I ran the same three files on the latest release of each line, 22.23.3, 24.21.0 and 26.10.0, on the M1 from the benchmark above:
| Node | node erasable.ts | node enum.ts | node --experimental-transform-types enum.ts |
|---|---|---|---|
| 22.23.3 | runs | ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX | runs, prints an experimental warning |
| 24.21.0 | runs | ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX | runs, prints an experimental warning |
| 26.10.0 | runs | ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX | node: bad option |
A parameter property (constructor(public x: number)) fails the same way on all three. So if a script only works because of that flag, the upgrade to 26 breaks it on the first line. Rewrite the enum as a const object and assign constructor fields by hand. Boring. Also permanent.
Don't wait for the runtime error, though. Put the guardrail where the editor and CI can see it, using the settings the Node.js TypeScript docs recommend:
{
"compilerOptions": {
"noEmit": true,
"target": "esnext",
"module": "nodenext",
"rewriteRelativeImportExtensions": true,
"erasableSyntaxOnly": true,
"verbatimModuleSyntax": true
}
}
Both typescript@6.0.3 and typescript@7.0.2 reject the enum and the parameter property with TS1294: This syntax is not allowed when 'erasableSyntaxOnly' is enabled. verbatimModuleSyntax covers the other trap: importing a type without the type keyword. Node throws The requested module './types.ts' does not provide an export named 'Id' on 22.23.3 and 26.10.0 alike, while TypeScript 7.0.2 flags it earlier as TS1484. Both flags behave the same in the native compiler, so nothing here blocks the TypeScript 7 migration guide.
Does the newer runtime start faster? No. Medians of 15 runs for a ten-line hello.ts were 53 to 57ms on 22.23.3, 56 to 58ms on 24.21.0 and 59 to 63ms on 26.10.0, against 32 to 34ms for bare node -e 0. That's inside the run-to-run noise. Upgrade for the support window, not for speed. Still on an older line? The migration guide under Related Tutorials covers the jump.
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. I measured this rather than quote someone else's benchmark. The script was a ten-line hello.ts, timed as wall-clock process start to exit, median of 15 runs after three warm-ups, on an Apple M1 with Node 22.23.2. Bare node -e 0 took about 30ms. Native --experimental-strip-types took 51 to 55ms across two runs, tsx 4.21 loaded with --import took about 160ms (its own CLI wrapper, about 180 to 220ms), and ts-node 10.9.2 with its default type checking took 730 to 830ms. Even ts-node in transpile-only mode sat near 200ms. Those are single-machine numbers and they moved between runs, so trust the ordering more than the digits. For a one-off script the difference barely registers, but for a test suite that spawns hundreds of short-lived processes, the saved milliseconds add up. The table below breaks down the three options side by side across startup cost, type checking behavior, and dependency footprint.
On one M1, a hello-world .ts file started in about 55ms with native strip-types, about 160ms with tsx and about 800ms with type-checking ts-node, a gap that mostly matters for monorepos spawning hundreds of short-lived script processes.
| ts-node 10.x | tsx 4.x | Native --experimental-strip-types | |
|---|---|---|---|
| Startup, hello-world (bare node ~30ms) | ~800ms | ~160ms | ~55ms |
| Type checking | Yes by default (skipped in transpile-only mode) | No | No |
| 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.