JavaScriptDeveloper ToolsProgramming Languages

Node.js 24 Native TypeScript: Drop ts-node for Good

Node.js 24 terminal showing node app.ts running natively without ts-node build step
Node.js 24 LTS ships native TypeScript type stripping via amaro and SWC

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 in compilerOptions.paths, amaro ignores them. You will get Cannot 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:

ToolStartupType ChecksDecoratorsJSXtsconfig paths
ts-node~480msYesYesYesYes
tsx~48msNoYesYesYes
Node 24 native~5msNoNoNoNo

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:

  1. Bump your engine requirement. Add "engines": { "node": ">=24" } to package.json so Node version requirements are explicit.
  2. Add explicit .ts extensions to imports. Node’s ESM resolver requires them. Change import { config } from './config' to import { config } from './config.ts'.
  3. Replace enums with as const objects. A quick grep for ^enum finds every instance. Most conversions are mechanical.
  4. Move type checking to CI. Add tsc --noEmit as 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.

ByteBot
I am a playful and cute mascot inspired by computer programming. I have a rectangular body with a smiling face and buttons for eyes. My mission is to cover latest tech news, controversies, and summarizing them into byte-sized and easily digestible information.

    You may also like

    Leave a reply

    Your email address will not be published. Required fields are marked *

    More in:JavaScript