HomeRunning TypeScript natively in Node.js: 22 vs 24 vs 26

Running TypeScript natively in Node.js: 22 vs 24 vs 26

By · Node.js & JavaScript developer
Published July 5, 2026

You can hand Node.js a TypeScript file and it just runs it — no tsc, no ts-node, no tsx, no build step. This is "type stripping": Node erases the type annotations and runs the JavaScript that's left. It's genuinely useful, but the exact behaviour has shifted across recent releases, and Node 26 quietly removed a feature that Node 22 and 24 still have. Here's the complete picture, with every command run on stock node:22-slim, node:24-slim, and node:26-slim Docker images.

$ docker run --rm node:22-slim node -v
v22.23.1
$ docker run --rm node:24-slim node -v
v24.18.0
$ docker run --rm node:26-slim node -v
v26.4.0

The good news: on all three, it just works

Type stripping has been on by default since Node 22.18 and 23.6 — no flag required — and became Stable in 24.12 / 25.2. So on every current release, a file using interfaces, generics, satisfies, and type annotations runs directly:

// erasable.ts
interface User { id: number; name: string; }

const greet = (user: User): string => `hi ${user.name} (#${user.id})`;

const u = { id: 26, name: 'node' } satisfies User;
console.log(greet(u));

function first<T>(arr: T[]): T | undefined { return arr[0]; }
console.log(first<number>([1, 2, 3]));

Same command, same output, on all three versions — and no experimental warning on any of them:

$ docker run --rm -v "$PWD":/w -w /w node:22-slim node erasable.ts
hi node (#26)
1

$ docker run --rm -v "$PWD":/w -w /w node:24-slim node erasable.ts
hi node (#26)
1

$ docker run --rm -v "$PWD":/w -w /w node:26-slim node erasable.ts
hi node (#26)
1

Node strips types — it does not check them

This is the single most important thing to understand. Node erases types; it never type-checks. A file with a blatant type error runs happily on all three versions:

// typeerror.ts
const n: number = 'not a number';
console.log(n);
$ docker run --rm -v "$PWD":/w -w /w node:26-slim node typeerror.ts
not a number

No error, no warning — the annotation is simply whitespaced away. Type checking is still your job; run tsc --noEmit in CI (or your editor). Running with Node and checking with tsc are two separate steps, on every version.

Importing other .ts files: the extension is mandatory

When one TypeScript file imports another, you must write the real .ts extension in the import specifier — not the extensionless style the TypeScript compiler lets you use. Type-only imports must be marked with the type keyword so Node knows to erase them.

// math.ts
export type Op = 'add' | 'sub';
export function calc(a: number, b: number, op: Op): number {
  return op === 'add' ? a + b : a - b;
}

// main.ts
import { calc, type Op } from './math.ts';   // note: ./math.ts, and `type Op`
const op: Op = 'add';
console.log(calc(2, 3, op));
$ docker run --rm -v "$PWD":/w -w /w node:24-slim node main.ts
5

Identical on 22 and 26. In your tsconfig.json, "verbatimModuleSyntax": true makes the compiler enforce the type keyword, and "rewriteRelativeImportExtensions": true lets tsc accept the .ts extensions.

The catch: strip-only can't handle every TypeScript feature

Because Node only strips, syntax that needs to emit real JavaScript isn't supported in the default mode. That includes enum, namespace with runtime code, parameter properties, import aliases, and decorators. Hit one and you get ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX:

// enum.ts
enum Color { Red, Green, Blue }
console.log(Color.Green);
$ docker run --rm -v "$PWD":/w -w /w node:26-slim node enum.ts
SyntaxError [ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX]: TypeScript enum is not supported in strip-only mode
    at parseTypeScript (node:internal/modules/typescript:57:40)
    ...
  code: 'ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX'
}

You get the same error on Node 22 and 24. Set "erasableSyntaxOnly": true in tsconfig.json and tsc will flag these features for you before you ever run the file.

The real difference: transform mode is gone in Node 26

Here's where the versions diverge. Node 22 and 24 shipped an experimental flag, --experimental-transform-types, that turns on full transformation so enum, namespaces, and parameter properties do work. On those versions it runs (with a warning):

$ docker run --rm -v "$PWD":/w -w /w node:24-slim node --experimental-transform-types enum.ts
1
(node:1) ExperimentalWarning: Transform Types is an experimental feature and might change at any time

Node 26 removed that flag entirely. The same command fails immediately — the option no longer exists:

$ docker run --rm -v "$PWD":/w -w /w node:26-slim node --experimental-transform-types enum.ts
node: bad option: --experimental-transform-types

So if your code relies on enum, runtime namespaces, parameter properties, or decorators, on Node 26 you have exactly two options: precompile with the TypeScript compiler (or a tool like tsx) first, or refactor to erasable syntax — for example, replace an enum with a const object plus a union type. For most modern codebases the erasable subset is all you need.

How we got here — the version timeline

Native TypeScript arrived incrementally. This is the short history, straight from the Node.js docs:

v22.6.0            type stripping added (behind a flag)
v22.7.0            --experimental-transform-types added
v22.18.0 / v23.6.0 stripping enabled by DEFAULT (no flag)
v24.12.0 / v25.2.0 stripping marked Stable
v26.0.0            --experimental-transform-types REMOVED

The practical takeaway per version:

  • Node 22 (LTS): node file.ts works by default; transform mode available via --experimental-transform-types.
  • Node 24 (LTS): same as 22, now marked stable; transform mode still available behind the flag.
  • Node 26 (Current): stripping is the whole story — transform mode is gone. Erasable syntax only, or precompile.

A tsconfig.json that matches how Node runs

Node ignores tsconfig.json when running — it doesn't do path aliases or downleveling — but the compiler still needs settings that agree with Node's behaviour for type checking. The Node docs recommend this baseline (TypeScript 5.8+):

{
  "compilerOptions": {
    "noEmit": true,
    "target": "esnext",
    "module": "nodenext",
    "rewriteRelativeImportExtensions": true,
    "erasableSyntaxOnly": true,
    "verbatimModuleSyntax": true
  }
}

A few more limits worth knowing

  • File extensions: .ts, .mts (always ESM), and .cts (always CommonJS) are supported. .tsx is not — JSX needs a real transform.
  • No .ts in node_modules: Node refuses to strip types from dependencies, so published packages must ship compiled JavaScript.
  • No source maps: since types are replaced with whitespace, line numbers are preserved and no source map is generated.
  • REPL and --check: TypeScript syntax isn't supported there.

Bottom line

For everyday scripts and services written in modern, erasable TypeScript, node file.ts is all you need on Node 22, 24, and 26 alike — drop ts-node and tsx from those workflows. Just remember two things: Node never type-checks, so keep tsc --noEmit in your pipeline; and if you're on Node 26 and still using enum or decorators, the --experimental-transform-types escape hatch is gone — move to erasable syntax or a build step.

Sources & further reading

About Code with Node.js

This is a personal blog and reference point of a Node.js developer.

I write and explain how different Node and JavaScript aspects work, as well as research popular and cool packages, and of course fail time to time.