Build1 publisher2 min readPublished
Node marks type stripping Stable for TypeScript written in erasable syntax
Node.js marks type stripping Stable in versions 25.2 and 24.12, letting node script.ts run TypeScript without ts-node, tsx or a build step. Node only deletes types, so the switch holds for code that stays inside TypeScript's erasable subset.
The Engineer · Build desk
What happened
- Node passes .ts, .mts and .cts files to amaro, a small SWC-based module that replaces type annotations, interfaces and type aliases with whitespace.
- Stripping performs no type checking, so a file full of type errors still runs as long as the JavaScript underneath is valid.
- Enums, namespaces with runtime values, constructor parameter properties and import aliases all generate JavaScript, and Node throws an error when it meets one.
- Node 26 removed the --experimental-transform-types flag that handled enums, and the docs now send users who need full TypeScript semantics to loaders such as tsx.
Compiled by The EngineerSomething wrong?How this is made
Why it matters
- decision Each enum or parameter property in a codebase is now a choice between a mechanical rewrite and keeping a third-party loader installed for good, with no core Node option in between.
- exposure If tsc --noEmit is not already in CI, deleting the build job leaves no stage in the pipeline that type checks the code before it runs.
- constraint Library authors still have to ship compiled JavaScript, because published packages land under node_modules where Node will not strip types.
- cost Projects that alias imports through tsconfig paths have to move every alias to # subpath imports in package.json before the loader can go.
The behaviour is older than the Stable label [2]. Type stripping has been on by default since Node 23.6 and 22.18 [1].
The design underneath is good engineering. According to a walkthrough published on dev.to, the whitespace replacement means the JavaScript that executes keeps every line and column of the file on disk [18]. Stack traces point at the right place without source maps [18].
Everything Node rejects follows from that design. A parameter property such as `constructor(private db: Database) {}` looks like an annotation. It actually generates an assignment, and no amount of deletion turns it into working code [17]. The erasable form declares `private db: Database;` as a field and assigns `this.db = db` in the constructor body [17]. The post says enums are the construct most teams trip over, and for them it suggests a const object marked `as const` plus a derived type. That keeps the autocomplete and gives a real runtime value [7]. The error, ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX, at least leaves no doubt about which rule was broken [6].
Imports fail less visibly. Node can only strip a type import it recognises as one, so a bare `import { User }` of an interface is treated as a value import [12]. It fails at runtime because nothing named `User` exists in the emitted JavaScript [12]. Writing `import type { User }`, or putting `type` inline inside the braces, avoids it [12].
The tsconfig the Node docs recommend sets `noEmit`, `target: esnext`, `module: nodenext`, `rewriteRelativeImportExtensions`, `erasableSyntaxOnly` and `verbatimModuleSyntax` [11]. That file is for tsc and the editor. Node never reads it, so `paths`, `target` downleveling and every other compiler setting are ignored at runtime [5]. The key line is `"erasableSyntaxOnly": true`. TypeScript 5.8 added it, and it turns every non-erasable construct into a compile error [10]. `verbatimModuleSyntax` makes tsc catch the bare type import before Node does [12]. With both flags on, one tsc run before uninstalling tsx lists every rewrite the codebase needs [10].
Two categories stay out entirely. Decorators are waiting on native JavaScript support, and .tsx files need a real JSX transform [9]. A codebase built on either keeps its loader or its build step [9].
The post scopes the feature to scripts, CLIs, internal services and test files. There, stripping plus the built-in test runner lets a small project drop ts-node, tsx and its build step [15]. That matches where I would use it. For a service one team owns, with tsc already running in CI, I'd accept a one-time rewrite of enums and imports to get the loader out of the dependency tree [15].
What to watch
- Native JavaScript decorators shipping in Node, the precondition the walkthrough gives for decorator-heavy code to run under type stripping.
- Whether TypeScript project templates start enabling erasableSyntaxOnly by default, which would shrink the pool of code that still needs a loader.
- Whether Node ever relaxes its refusal to strip types under node_modules, the rule that keeps a compile step in every published library.