Run TypeScript in Node.js Without a Build Step: Type Stripping Tested on Node 22, 24 and 26
Cross-posted from my blog. The original, with any updates, lives on fabwaseem.com. You can now run a .ts file with plain node app.ts, no tsx, no ts-node, no build step. Type stripping has been on by default since Node.j
Cross-posted from my blog. The original, with any updates, lives on fabwaseem.com.
You can now run a .ts file with plain node app.ts, no tsx, no ts-node, no build step. Type stripping has been on by default since Node.js 22.18.0 and 23.6.0, and it became stable in Node.js 24.12.0 and 25.2.0. On Node.js 26 LTS it is simply how Node works.
But "Node runs TypeScript" has sharp edges. Enums fail. Parameter properties fail. Decorators fail. Imports without .ts fail. And Node never checks your types. I tested every one of these on Node.js 22.20.0, 24.22.2 and 26.13.0, and this post shows the exact errors, the tsconfig.json that catches all of them before runtime, and how startup time compares to tsx and precompiled JavaScript.
How Node.js type stripping works
Node replaces TypeScript type annotations with whitespace and runs what is left as JavaScript. It does not type-check, it ignores tsconfig.json, and it only accepts erasable syntax: TypeScript that disappears completely when you delete the types.
From the Node.js TypeScript docs history table:
| Node.js version | Change |
|---|---|
| v22.6.0 | Type stripping added behind --experimental-strip-types
|
| v23.6.0, v22.18.0 | Enabled by default |
| v24.3.0, v22.18.0 | No more experimental warning |
| v24.12.0, v25.2.0 | Marked Stable |
| v26.0.0 |
--experimental-transform-types flag removed
|
You can check it at runtime: node -p "process.features.typescript" printed strip on all three versions I tested.
What works out of the box
This file ran unchanged with node ok.ts on Node 22.20, 24.22 and 26.13:
// ok.ts
interface User { id: number; name: string }
type Role = 'admin' | 'user';
import type { Stats } from 'node:fs';
import { greet } from './util.ts';
const u: User = { id: 1, name: 'Waseem' };
const role: Role = 'admin';
function size<T>(xs: T[]): number { return xs.length; }
const s = undefined as Stats | undefined;
console.log(greet(u.name), role, size([1, 2, 3]), s);
// hi Waseem admin 3 undefined
Interfaces, type aliases, generics, as casts, import type and annotated functions are all erasable, so they just work. Add "type": "module" to package.json; without it, Node prints a MODULE_TYPELESS_PACKAGE_JSON warning and reparses the file as ESM.
What fails (tested on Node 22, 24 and 26)
All three versions gave the same result for every case below.
| TypeScript feature | What Node prints |
|---|---|
enum Color { Red, Green } |
SyntaxError [ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX]: TypeScript enum is not supported in strip-only mode |
namespace A { export const x = 1 } |
SyntaxError [ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX]: TypeScript namespace declaration is not supported in strip-only mode |
constructor(public name: string) |
SyntaxError [ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX]: TypeScript parameter property is not supported in strip-only mode |
import { greet } from './util' |
Error [ERR_MODULE_NOT_FOUND]: Cannot find module '.../util' |
import { User } from './types.ts' (User is an interface) |
SyntaxError: The requested module './types.ts' does not provide an export named 'User' |
@log class K {} |
SyntaxError: Invalid or unexpected token |
A .tsx file |
TypeError [ERR_UNKNOWN_FILE_EXTENSION]: Unknown file extension ".tsx" |
A .ts file inside node_modules
|
Error [ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING] |
Three of these surprise people the most:
-
Type-only imports must say
import type. Node removes the type but keeps the import statement, so it looks for a runtime export calledUserand crashes. -
Decorators don't run. That means NestJS, TypeORM entities and anything with
@Injectable()still needs a compile step or a loader liketsxor SWC. -
Packages can't ship raw
.ts. Node refuses to strip types undernode_modules, so libraries still need to publish JavaScript.
--experimental-transform-types is gone in Node 26
On Node 24, node --experimental-transform-types enum.ts ran the enum (with an ExperimentalWarning). On Node 26.13.0 the same command printed:
node: bad option: --experimental-transform-types
If you relied on that flag for enums, Node 26 is the point where you have to remove them or go back to a loader.
The tsconfig.json that catches every failure
Node ignores tsconfig.json, but your editor and tsc don't. With this config, tsc flagged every runtime failure above (except decorators, see below) before I ran anything:
{
"compilerOptions": {
"target": "esnext",
"module": "nodenext",
"noEmit": true,
"allowImportingTsExtensions": true,
"erasableSyntaxOnly": true,
"verbatimModuleSyntax": true,
"strict": true,
"skipLibCheck": true
}
}
Output from tsc -p . with TypeScript 5.9.3:
enum.ts(1,6): error TS1294: This syntax is not allowed when 'erasableSyntaxOnly' is enabled.
ns.ts(1,11): error TS1294: This syntax is not allowed when 'erasableSyntaxOnly' is enabled.
paramprop.ts(1,23): error TS1294: This syntax is not allowed when 'erasableSyntaxOnly' is enabled.
noext.ts(1,23): error TS2835: Relative import paths need explicit file extensions in ECMAScript imports when '--moduleResolution' is 'node16' or 'nodenext'.
typeimport.ts(1,10): error TS1484: 'User' is a type and must be imported using a type-only import when 'verbatimModuleSyntax' is enabled.
What each option does:
-
erasableSyntaxOnly(TypeScript 5.8+) errors on enums, namespaces and parameter properties. -
verbatimModuleSyntaxforcesimport typefor type-only imports. -
allowImportingTsExtensionslets you write./util.ts, which is what Node needs. It requiresnoEmit(orrewriteRelativeImportExtensionsif you also emit). -
module: nodenextmakestscrequire file extensions like Node does.
The gap: decorators. TypeScript 5.9 accepts standard decorators, so tsc passed dec.ts, but Node 26 still threw a SyntaxError. If your code uses decorators, keep a build step for that code.
Run tsc --noEmit in CI and in a pre-commit hook. Node won't check types for you.
Replacing enums
The usual erasable replacement is a const object plus a type:
export const Color = { Red: 0, Green: 1 } as const;
export type Color = (typeof Color)[keyof typeof Color];
Startup time: node vs tsx vs precompiled JS
Same ok.ts (two small files), Node 26.13.0, 30 interleaved runs each, wall time for the whole process:
| How it ran | Median | p90 |
|---|---|---|
node js/ok.js (precompiled with tsc) |
27.7 ms | 33.0 ms |
node ok.ts (type stripping) |
62.6 ms | 67.2 ms |
tsx ok.ts (tsx 4.23.15) |
116.9 ms | 128.5 ms |
Built-in stripping was about half the startup time of tsx, but about 35 ms slower than running precompiled JavaScript on this machine. For scripts, CLIs, dev servers and tests that difference doesn't matter. For a serverless function where cold start counts, shipping compiled JavaScript is still faster.
When to use built-in type stripping
Use node file.ts for:
- Scripts, seeds, migrations and one-off tools
- CLIs and internal services you control end to end
- Running tests with
node --teston.tsfiles - Dev mode with
node --watch src/server.ts
Keep a build step (tsc, SWC, esbuild or tsx) for:
- NestJS and anything else that uses decorators
- React and
.tsxfiles - Libraries you publish to npm
- Code that relies on
pathsaliases intsconfig.json(Node ignores them) - Serverless functions where cold start matters
If you're upgrading Node at the same time, see Node 24 vs Node 26 for the other breaking changes, and TypeScript 7 for where the TypeScript compiler itself is going.
FAQ
Can Node.js run TypeScript directly?
Yes. Since Node.js 22.18.0 and 23.6.0, node file.ts works without a flag, and the feature is marked stable from Node.js 24.12.0 and 25.2.0. Node only strips types; it does not type-check and it does not support enums, namespaces, parameter properties or decorators.
Does Node.js type stripping check types?
No. Node replaces types with whitespace and runs the result. A file with type errors runs fine as long as the JavaScript is valid. Run tsc --noEmit separately to check types.
Why does my enum fail in Node.js?
Enums generate runtime code, so they aren't erasable. Node prints ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX: TypeScript enum is not supported in strip-only mode. Replace them with a const object and a union type, and turn on erasableSyntaxOnly so tsc warns you.
Is --experimental-transform-types available in Node 26?
No. It was removed in Node.js 26.0.0. On Node 26.13.0 the flag fails with bad option: --experimental-transform-types. It still works with a warning on Node 24.
Do I still need tsx or ts-node?
Only for code that uses non-erasable syntax, decorators, .tsx, or tsconfig paths. For plain erasable TypeScript, node is enough and started about twice as fast as tsx in my test.
Why do I need .ts in import paths?
Node resolves files exactly like it does for JavaScript ESM: no extension guessing. import './util' fails with ERR_MODULE_NOT_FOUND; write import './util.ts' and enable allowImportingTsExtensions in tsconfig.json.
Originally published on fabwaseem.com.
Originally published by Dev.to WebDev. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.