
Node 24, the current LTS, now runs .ts files natively. No ts-node. No tsc before you can test anything. No build step standing between you and execution. You write TypeScript, you run node server.ts, and it works. This is not an experiment — it is the default behavior of the runtime millions of developers already use.
Most developers haven’t made the switch yet. That’s a mistake worth correcting.
What Changed and Why It’s Fast
Node 24 ships with a module called amaro — a thin wrapper around SWC’s WebAssembly TypeScript parser. When you run a .ts file, amaro intercepts it, strips every type annotation, interface, generic, and as cast from the source, and hands plain JavaScript to V8. The key detail: types are replaced with whitespace, not removed. Line numbers are preserved. Stack traces work exactly as you’d expect.
What amaro does not do is type-check your code. Node has no interest in whether your types are correct — that’s still tsc’s job. But because amaro only strips instead of compiling, it is orders of magnitude faster than ts-node, which spins up a full TypeScript compiler on every boot.
The numbers are real: on a 600-line Express server, cold start drops from ~1.4 seconds with ts-node to under 200 milliseconds with native stripping — a 9x improvement. For serverless functions where cold starts are billed and user-facing, that gap is the difference between a snappy API and one that frustrates users before it even answers.
The Catch: Not All TypeScript Is Erasable
Node’s type stripping works only on what the TypeScript team calls “erasable syntax” — syntax that can be removed without altering the runtime behavior of the remaining JavaScript. That covers the vast majority of modern TypeScript: type annotations, type aliases, interfaces, generic parameters, as casts, the satisfies keyword, and declare statements.
What it does not cover: enum and value-exporting namespace blocks. These features generate JavaScript objects at runtime — stripping the keyword would leave broken code. Node 26 removed the --experimental-transform-types flag that previously handled them. That flag is gone. Enums are now a hard stop in native mode.
The migration path is straightforward. Replace enums with as const objects:
// Before — breaks in native Node TypeScript mode:
enum Direction {
Up = 'UP',
Down = 'DOWN'
}
// After — fully erasable:
const Direction = {
Up: 'UP',
Down: 'DOWN'
} as const
type Direction = typeof Direction[keyof typeof Direction]
TypeScript 5.8 added the erasableSyntaxOnly compiler flag to enforce this at the type-check level — it flags enums and value namespaces as errors before you even run the code. Add it to your tsconfig and your CI will surface what needs migrating before Node does.
Your New Workflow
The before/after is stark:
# Before Node 24:
npm install -D typescript ts-node @types/node
npx ts-node src/index.ts
# After Node 24:
node src/index.ts
Remove ts-node from your devDependencies. Replace nodemon with Node’s built-in --watch flag. Your dev script becomes node --watch src/index.ts.
Keep tsc. Run it in CI with tsc --noEmit to catch type errors before they ship. If you’re publishing a library, run it to emit declaration files. For application development, you no longer need to compile.
One import requirement to know: native TypeScript mode requires .ts extensions in relative imports, not .js:
// Won't work in native mode:
import { User } from './models/user.js'
// Correct:
import { User } from './models/user.ts'
A minimal tsconfig.json for native mode:
{
"compilerOptions": {
"target": "ESNext",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"erasableSyntaxOnly": true,
"noEmit": true
}
}
The Mental Model Worth Internalizing
The shift here is not just removing a dependency. It’s a different relationship with the build step. TypeScript’s original pitch was “types for JavaScript” — but the tax was a mandatory compile-before-run loop that JavaScript developers never had to pay. Native stripping eliminates that tax for local development and production execution alike.
The new division of labor: node executes your code. tsc checks your types. They are now genuinely separate concerns, and you can run them independently. This is how Deno and Bun have worked for years. Node, the runtime with the largest production install base, has joined them.
The remaining question is enums. If your codebase has significant enum usage, the migration is real work — as const is ergonomically equivalent for string enums but lacks automatic reverse mappings for numeric enums. For large codebases, tools like jscodeshift can automate the conversion. The TypeScript team’s direction is clear: erasable-only is the standard going forward. The earlier you start, the smaller the migration debt.
What to Do Today
- Add
erasableSyntaxOnly: trueto your tsconfig - Run
tsc --noEmit— fix any enum or namespace errors it surfaces - Remove
ts-nodefrom your package.json - Update your dev script to
node --watch src/index.ts - Update relative imports to use
.tsextensions
The canonical references: Node.js TypeScript module documentation and the amaro repository on GitHub. For the TypeScript side, Total TypeScript’s coverage of erasableSyntaxOnly is the clearest explanation of what the compiler flag does and why it matters now. For a detailed runtime comparison including Bun and Deno, LogRocket’s tsx vs ts-node vs native breakdown is worth reading before you finalize your toolchain decision.
The build step was always a workaround. Node 24 made it optional. The only thing left is to take advantage of it.













