
Node.js 24 is LTS, and it ships with something the JavaScript community has wanted for a decade: run TypeScript files directly with node app.ts — no ts-node, no tsx, no tsc compilation step. Before you rip out your entire build toolchain, you need to understand what “native TypeScript support” actually means here. Node strips your types. It does not check them. That distinction matters more than most coverage admits.
How It Works: Type Stripping Under the Hood
Node.js 24 uses a module called amaro — a thin wrapper around @swc/wasm-typescript, a WebAssembly-compiled version of the SWC TypeScript parser. When you run node app.ts, amaro scans the file, replaces every type annotation with whitespace (preserving line and column positions so your stack traces still make sense), and hands the result to V8. No type resolution. No constraint checking. No declaration files emitted.
This makes it fast. Extremely fast. ts-node boots a full TypeScript compiler on every invocation, which takes roughly 480ms on a cold start. Node 24’s native stripping runs in under 5ms. For a script you run dozens of times a day — or a Lambda that cold-starts on every request — that gap is real money. The official Node.js TypeScript documentation confirms the feature is now stable and on by default for .ts, .mts, and .cts files.
There is no flag to add, no package to install. Upgrade to Node 24, and node app.ts just works.
What Breaks: The Four Non-Negotiables
Here is where most “native TypeScript” takes mislead developers. Amaro — the open-source module powering Node’s TypeScript support — ignores tsconfig.json entirely and only handles erasable syntax: annotations that can be removed by replacing them with whitespace. Four common patterns are not erasable:
- Enums.
enum Status { Active, Inactive }emits actual JavaScript runtime code. Amaro cannot simply erase it. Node will throw a parse error. - Decorators.
@Injectable(),@Column()— if your codebase uses decorators (NestJS, TypeORM, MikroORM), Node’s native stripping will not run them. You will get a parser error. - JSX/TSX. Node does not strip JSX syntax. If you are writing React components, Next.js pages, or Remix loaders — your framework bundler handles TypeScript, not Node directly. This feature is irrelevant to you.
- tsconfig path aliases. If your project uses
@app/*or@utils/*path mappings incompilerOptions.paths, amaro ignores them. You will getCannot find module '@app/config'errors at runtime.
The enum case is the most surprising because enums are so common. The fix is straightforward:
// Before — breaks with native Node
enum Status {
Active,
Inactive,
}
// After — works with native Node
const Status = {
Active: 0,
Inactive: 1,
} as const;
type Status = typeof Status[keyof typeof Status];
It is more verbose, but as const objects have been the community-preferred alternative to enums for years. This migration is a reasonable forcing function to clean up a pattern TypeScript itself has been moving away from.
The Tool Decision Tree
Native Node.js TypeScript is not a universal ts-node replacement. It replaces ts-node for the specific category of projects that do not need decorators, JSX, or path aliases. Here is how the tools compare:
| Tool | Startup | Type Checks | Decorators | JSX | tsconfig paths |
|---|---|---|---|---|---|
| ts-node | ~480ms | Yes | Yes | Yes | Yes |
| tsx | ~48ms | No | Yes | Yes | Yes |
| Node 24 native | ~5ms | No | No | No | No |
The decision is clear: if you are building backend scripts, CLI tools, simple serverless functions, or any plain Node.js service that does not use decorators or JSX — drop ts-node today and use native Node. If you are on NestJS, TypeORM, or a JSX framework, tsx is the right ts-node replacement (10x faster than ts-node, handles decorators, reads tsconfig).
Migrating Off ts-node in Four Steps
For projects that qualify, the migration takes under ten minutes:
- Bump your engine requirement. Add
"engines": { "node": ">=24" }topackage.jsonso Node version requirements are explicit. - Add explicit
.tsextensions to imports. Node’s ESM resolver requires them. Changeimport { config } from './config'toimport { config } from './config.ts'. - Replace enums with
as constobjects. A quick grep for^enumfinds every instance. Most conversions are mechanical. - Move type checking to CI. Add
tsc --noEmitas a dedicated CI step. This is actually better practice — explicit, non-blocking on your dev loop, and will get significantly faster when TypeScript 7.0’s Go-based compiler ships.
Remove ts-node and any associated type-override workarounds from devDependencies once migration is verified. Your package.json gets lighter and your CI gets faster.
The Bigger Picture
Type stripping in the runtime is the right architectural call. Mixing type checking into the execution path was always a convenience hack — useful when the tooling ecosystem was immature, now a legacy pattern. The cleaner approach is explicit: run code fast, verify types separately in CI. Node.js is not the first runtime here (Deno and Bun both support type stripping), but carrying this into LTS means it is now the baseline assumption for backend TypeScript development.
The build step is not dead for everyone. But for the right project, it just became optional.













