Key Takeaway
Type stripping lets Node.js run TypeScript files directly by erasing type annotations and replacing them with whitespace, so no tsc build step is needed. It performs no type checking. It arrived behind --experimental-strip-types in Node 22.6 and runs unflagged from Node 23.6. Enums and namespaces need transform mode.
For years the honest answer to how you run TypeScript on the server was: you do not, not really. You run JavaScript that a build step produced from TypeScript. Every project carried a tsc invocation, or ts-node, or a bundler, sitting between the file you wrote and the process that actually executed. That extra hop is exactly what Node.js has now removed for a large class of code.
The mechanism is called type stripping, and the mental model is simpler than any transpiler. Node does not understand your types, compile them, or reason about them. It deletes them. What is left is plain JavaScript, and that is what the V8 engine runs. Once you internalise that one sentence, every capability and every limitation of the feature follows from it.
What type stripping actually does
When Node encounters a .ts file, it hands the source to an internal transform that removes erasable TypeScript syntax and puts whitespace in its place. A line like const port: number = 3000 becomes const port = 3000. The colon and the type annotation are gone; the character positions are preserved. That whitespace trick matters: because nothing shifts, stack traces still point at the correct line and column in your original file without needing a source map.
Crucially, no type checking happens. Node strips the annotations and moves on. If you assigned a string to that number-typed variable, Node will not complain, because by the time it runs the code the type information no longer exists. Type stripping gives you the run step for free but not the safety net. The commands below show the flag history and the compiler call you still need in CI.
# Node 22.6 through 22.17: type stripping is behind a flag
node --experimental-strip-types app.ts
# Node 22.18 / 23.6 and later: no flag, just run the file
node app.ts
# Opt back out if some tool needs the old behavior
node --no-experimental-strip-types app.ts
# Node does NOT type check — run the compiler yourself in CI
npx tsc --noEmitThe timeline: from a flag to on by default
This landed in stages, and knowing which version does what saves confusion when a script that runs on your laptop fails on an older CI image:
- Node 22.6.0, released August 2024, introduced --experimental-strip-types for the first time. You had to opt in explicitly to run a .ts file.
- Node 22.7.0 added a second, broader mode, --experimental-transform-types, for the features plain stripping cannot handle.
- Node 23.6.0, and the backport in 22.18.0, enabled type stripping by default. From those versions you can run node file.ts with no flag at all.
- In Node 25.2.0 and 24.12.0 the feature reached stable, and --no-experimental-strip-types remains available if you ever need to turn it off.
What it does not do: no type checking
This is the single most important thing to tell your team before they lean on it. Running node app.ts is not the same as running tsc. Node will happily execute code riddled with type errors, because it never looked at the types in the first place. Type stripping replaces the transpile step, not the verification step. Your editor still checks types as you write, but the runtime does not, and neither does your pipeline unless you make it.
Keep a real type check in CI. Type stripping removes the build, not the compiler. Run npx tsc --noEmit as its own pipeline stage so a type error fails the build before it ships. The recommended tsconfig for this setup uses noEmit true with target esnext and module nodenext, since tsc now only validates and Node does the running.
Erasable versus non-erasable syntax
Type stripping only works on syntax that can be deleted and leave valid JavaScript behind. Type annotations, interfaces, type aliases, and import type statements are all erasable: take them out and the remaining code stands on its own. Some TypeScript features are different. An enum, a namespace containing runtime code, parameter properties in a constructor, and import aliases all need to be replaced by new JavaScript, not merely removed. Plain type stripping cannot do that, so Node throws ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX.
// Erasable syntax — runs under plain type stripping
const port: number = 3000
interface User { id: string; name: string }
import type { IncomingMessage } from "node:http"
// Non-erasable syntax — throws ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX
enum Role { Admin, User }
namespace Api { export const version = 1 }
class Service { constructor(private readonly db: DB) {} }
// Run non-erasable code by asking Node to transform it instead:
node --experimental-transform-types app.ts
// Or lock your codebase to strip-safe syntax in tsconfig.json:
// "erasableSyntaxOnly": trueIf your codebase depends on enums or namespaces with runtime code, plain node file.ts will throw. Either run it with --experimental-transform-types, refactor those constructs into erasable equivalents such as a plain object with as const, or set erasableSyntaxOnly true in tsconfig so the compiler flags non-erasable syntax at author time rather than letting it fail at runtime.
Transform mode for enums and namespaces
When you genuinely need those non-erasable features, --experimental-transform-types switches Node from deleting types to actually transforming them into equivalent JavaScript. It handles enums, namespaces with runtime code, and similar constructs that plain stripping rejects. Because transform mode rewrites code rather than blanking it out, character positions move, so pair it with --enable-source-maps to keep stack traces honest. It is heavier than stripping, which is why the lightweight strip-only path is the default.
What runs it under the hood: amaro and swc
Node does not ship a TypeScript parser of its own. The internal loader is a package called amaro, which wraps the WebAssembly build of swc, the Rust-based TypeScript and JavaScript toolchain. swc does the fast parse-and-strip work; amaro is the thin seam Node uses to call it and to let the bundled TypeScript version be upgraded independently of Node itself. That Rust core is why stripping adds so little startup overhead compared with a JavaScript-based transpiler.
Type stripping versus ts-node and a tsc build
| Aspect | Node type stripping | ts-node or tsc build |
|---|---|---|
| Type checking | None — annotations erased, never verified | Full type checking via the compiler |
| Build step | None — run the .ts file directly | A transpile step or long-lived runner |
| Startup overhead | Near zero — Rust-based swc via amaro | Slower — JS transpile or a build stage |
| Enums and namespaces | Rejected unless transform mode is on | Supported out of the box |
| Best fit | Scripts, small services, the dev loop | Large apps using the full TypeScript feature set |
My rule of thumb is straightforward. For scripts, tooling, and services written in erasable syntax, I run them with node directly and let a separate tsc --noEmit stage catch type errors in CI. For codebases leaning on enums, namespaces, or decorators, I either reach for transform mode or keep a real build. Either way, the days of a mandatory build step just to execute a .ts file on the server are over, and that is a genuinely nice thing to stop thinking about.
Sources
- Node.js docs — Modules: TypeScript (type stripping, supported and unsupported syntax)
- Node.js 22.6.0 release notes — introduces --experimental-strip-types
- Node.js 23.6.0 release notes — type stripping enabled by default
- nodejs/amaro — the swc-based TypeScript loader used internally by Node
- Node.js Learn — Running TypeScript Natively

