Running TypeScript natively in Node.js: 22 vs 24 vs 26
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.0The 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)
1Node 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 numberNo 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
5Identical 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 timeNode 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-typesSo 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 REMOVEDThe practical takeaway per version:
- Node 22 (LTS):
node file.tsworks 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..tsxis not — JSX needs a real transform. - No
.tsinnode_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.