I ran node server.ts by accident last Tuesday. Fat-fingered the command — I meant to type npx tsx server.ts like I've done for years. It just... worked. No error. No missing loader. The server started, and I sat there staring at my terminal like someone who just discovered their car has a heated steering wheel.

That was the moment I realized the JavaScript ecosystem had quietly changed underneath me while I was busy arguing about frameworks.

Type Stripping, or How Node Learned to Ignore Your Types

Node.js v22.18.0 (and v24.3.0) shipped stable TypeScript support through a mechanism called type stripping. The name is literal: Node reads your .ts file, replaces every type annotation with whitespace, and runs what's left as JavaScript. No transpilation. No code generation. No tsconfig.json consultation.

The engine behind this is Amaro, a lightweight module that does exactly one thing — erase types. It doesn't check them (that's still tsc's job). It doesn't transform syntax. It deletes the parts that aren't JavaScript and leaves everything else untouched.

// What you write
function add(a: number, b: number): number {
  return a + b;
}

// What Node actually executes (conceptually)
function add(a        , b        )         {
  return a + b;
}

The whitespace replacement is clever. It preserves source maps and line numbers without generating a separate .js file. Your stack traces still point to the right line in your .ts source. No sourcemap wrangling, no "oh that's the compiled output line number" moments.

In earlier versions (v22.6.0 through v22.17.x), you needed to pass --experimental-strip-types explicitly. That flag still exists, but on v22.18+ and v24.3+, type stripping is enabled by default. You just run node yourfile.ts and move on with your life.

What This Does to Your package.json

A typical Node + TypeScript project in 2024 had a small constellation of dev dependencies dedicated entirely to the problem of "please run my .ts files." That constellation just collapsed.

Before:

{
  "scripts": {
    "dev": "nodemon --exec tsx src/index.ts",
    "build": "tsc",
    "start": "node dist/index.js"
  },
  "devDependencies": {
    "tsx": "^4.19.0",
    "nodemon": "^3.1.0",
    "typescript": "^5.7.0"
  }
}

After:

{
  "scripts": {
    "dev": "node --watch src/index.ts",
    "build": "tsc",
    "start": "node dist/index.js"
  },
  "devDependencies": {
    "typescript": "^5.7.0"
  }
}

Two fewer packages. One fewer abstraction layer between you and your code. node --watch handles file watching natively, Amaro handles the .ts files. The dev script is now three words.

The Enum-Shaped Hole

There's a catch, and if you've worked with TypeScript long enough, you can probably guess what it is.

Type stripping only works on syntax that can be erased without changing runtime behavior — interfaces, type aliases, generics, return type annotations, everything you'd put in a .d.ts file. But enum declarations generate actual JavaScript code. So do namespace blocks with runtime values and parameter properties in constructor shorthand.

For those features, there's a separate flag:

node --experimental-transform-types server.ts

Notice the "experimental" — this one hasn't graduated to stable yet. If your codebase relies heavily on enums, you're stuck with the flag for now.

My recommendation: switch to as const objects. They produce the same result, they're fully type-safe, and they don't need any transformation because they're already valid JavaScript.

// Instead of enum:
enum Status { Active, Inactive, Pending }

// Use const objects:
const Status = { Active: 0, Inactive: 1, Pending: 2 } as const;
type Status = (typeof Status)[keyof typeof Status];

The TypeScript team has been quietly steering people toward this pattern for a while. Now there's a concrete runtime reason to listen.

Do You Still Need tsx?

Honest answer: depends on your project.

Scenario Native Node tsx ts-node
Simple scripts Works perfectly Overkill Overkill
Watch mode --watch flag Built-in Needs nodemon
Enums / namespaces Needs extra flag Handles it Handles it
Path aliases (@/lib) Not supported Supported Supported
Type checking at runtime No No Yes (slow)
Production Build with tsc Don't ship this Don't ship this

tsx still earns its keep if you use path aliases or rely on enum syntax. It's backed by esbuild under the hood, so every TypeScript feature works without flag arithmetic. For a project that sticks to modern patterns — no enums, no namespaces, standard import paths — the native runner handles everything.

As for ts-node: the project hasn't shipped a release in over two years. The codebase is effectively unmaintained. If you're still depending on it, now is the time to migrate. You have two good options; there's no reason to keep a third one on life support.

A New Project in 2026

Here's what a zero-dependency TypeScript setup looks like now:

mkdir my-api && cd my-api
npm init -y
npm i -D typescript @types/node
npx tsc --init

Write your code in .ts files and run it directly:

node src/index.ts

For CI, add tsc --noEmit to your lint step so types still get checked. For production builds, compile with tsc and ship the JavaScript output. That's the entire toolchain — no loader config, no register hooks, no esbuild pipeline, no "which tsconfig extends which" debates at 3 AM.

The gap between writing TypeScript and running TypeScript is now exactly zero extra commands.

The Quiet Part

This change didn't get a splashy launch. No one's going to build a conference talk around "node can run .ts files." It's not the kind of feature that creates Twitter arguments or spawns competing blog posts about whether it's actually good.

But delete your tsx dependency. Remove the nodemon config. Strip out the ts-node/register hook you forgot was still in your test runner config. Count the lines of configuration that just became unnecessary.

Sometimes the best releases are the ones that make you uninstall things.