TypeScript's Erasable Syntax: Running .ts Without a Build Step
For a decade, the standard TypeScript workflow has been a quiet compromise: you write type annotations, a transpiler strips them, and you ship JavaScript. The stripping was invisible, but it always carried a cost — a build step, a source map, a dependency graph, and a few extra milliseconds on every cold start. In 2026 that compromise is finally dissolving. Node.js, Deno, and Bun have all standardized on native type stripping, and TypeScript itself has added the erasableSyntaxOnly flag to police the boundary. The result is a workflow where your editor, your runtime, and your type checker all agree on the same file — with no transpilation in between.
What "erasable" actually means
The core insight is that most TypeScript syntax is purely a compile-time concern. Type annotations, interfaces, type aliases, generics, and type assertions carry no runtime meaning. A type annotation on a parameter is, at runtime, just whitespace. The TypeScript compiler has always exploited this fact; it erases types as a side effect of emitting JavaScript. Native type stripping simply moves that erasure into the runtime, so Node can execute a .ts file directly.
// hello.ts
function greet(name: string): string {
return `Hello, ${name}`;
}
console.log(greet("world"));
Node runs this file as-is. The : string annotation is stripped to whitespace, the file is executed, and no tsc invocation ever happens. Type checking still occurs — but it is a separate, explicit step in CI or your editor, decoupled from execution.
The config that makes it safe
Getting editor and runtime to agree requires a strict tsconfig.json. The community has converged on a specific combination: target set to esnext, module set to nodenext, allowImportingTsExtensions enabled so you can import .ts files by their actual paths, rewriteRelativeImportExtensions to keep those paths stable, and verbatimModuleSyntax to keep type-only imports explicit. The lynchpin is erasableSyntaxOnly, which makes tsc reject anything the runtime cannot strip.
{
"compilerOptions": {
"target": "esnext",
"module": "nodenext",
"allowImportingTsExtensions": true,
"rewriteRelativeImportExtensions": true,
"verbatimModuleSyntax": true,
"erasableSyntaxOnly": true,
"noEmit": true
}
}
With this in place, if your code compiles under tsc --noEmit, Node is guaranteed to be able to run it. That contract is the entire value of the feature: one file, two consumers, zero divergence.
What gets left behind
Erasable syntax is deliberately narrow. Everything with a runtime footprint must go. enum declarations generate runtime objects, so they are disallowed. namespace blocks carry runtime semantics and are rejected. Parameter properties in constructors — the constructor(private x: number) shorthand — emit runtime field assignment, so they are out. Decorators, which execute at runtime, are excluded unless you run with an explicit transform. The migration cost is concentrated here: codebases that leaned on enums and parameter properties must rewrite every one of those declarations before adopting erasable-only syntax. Run tsc --noEmit --erasableSyntaxOnly and the compiler will enumerate exactly what needs to change.
Node native support and flags
Node has been shipping type stripping since version 22.6, where it first appeared behind a flag. It was unflagged in 23.x, meaning the feature is on by default in Node 23 and every later release, including Node 24 and Node 26. The default behavior is permissive: Node executes TypeScript files that contain only erasable syntax, replacing annotations with whitespace, and performs no type checking. If you need the stricter contract, disable stripping with --no-strip-types and rely on your own typecheck pass. In production, teams report running the feature with zero issues on Node 22, provided their typecheck is configured with erasableSyntaxOnly and friends. The ecosystem consensus is clear: Bun, Deno, and Node all standardized on stripping over transpilation.
Where it does not apply
The caveats matter. Type stripping is a Node-runtime feature, so projects targeting Node 22.5 or earlier cannot use it — there the flag provides no benefit because tsc still handles transpilation. Browser applications are a different story entirely. Frontend bundles must be downleveled to older ECMAScript targets and rewritten through a bundler, so a browser target still needs its full build pipeline; the runtime cannot strip types the way Node does. And any code that relies on enum, const enum, or namespace blocks with runtime code must be rewritten before adopting erasable-only syntax. These constraints define the adoption boundary: native stripping is a server-side and CLI win, not a universal replacement for your bundler.
Why you should care
The practical payoff shows up in measurable ways. Development servers start faster because there is no transpilation step on every file change; the runtime executes the source you are actually editing. Cold starts in serverless functions drop, since deployment artifacts can be plain .ts files with no compiled output to generate. Debugging improves because stack traces and line numbers map directly to the source you write, with no source-map indirection. And the dependency tree shrinks: you remove the transpiler from the hot path and keep type checking as a separate, explicit quality gate rather than a build-time side effect.
Adopting it in production
The migration is incremental. Start by enabling erasableSyntaxOnly and fixing the declarations it rejects — this is the bulk of the work. Then switch your Node entry points to execute .ts files directly, and remove the emit step from your development workflow while keeping tsc --noEmit in CI. Keep the bundler for browser delivery and for production artifacts that still need downleveling. The long-term trend favors erasable syntax: every major runtime has standardized on it, and TypeScript's own compiler is steering the ecosystem toward it. The era of "write TypeScript, ship JavaScript" is not ending — it is finally collapsing the two steps into one.
The takeaway
Native type stripping is not a gimmick or a niche flag. It is the recognition that types were always meant to be erased, not compiled. By making the runtime the eraser, Node and its peers remove the build step from the loop entirely, and erasableSyntaxOnly gives you a compile-time guarantee that your editor and your runtime will never disagree. For Node services, CLIs, and serverless functions, the answer to "do I still need a build step?" is increasingly a quiet no.
Comments