Node 24 原生跑 .ts 实测:strip-only 能跑什么,enum、namespace 和参数属性为什么报错
Node 24 原生跑 .ts 实测:strip-only 能跑什么,enum、namespace 和参数属性为什么报错
Node.js 24 里,node app.ts 已经不需要任何 flag、不需要 ts-node、不需要先 tsc 编译。但它做的不是"编译 TypeScript",而是把类型标注擦掉后直接交给 V8,也就是 Node 报错原文里写的 strip-only mode。擦得掉的和擦不掉的,边界在哪,报错长什么样,我在 Windows 上用 Node v24.9.0 把十来种写法逐个跑了一遍。
结论先放在这里:接口、类型别名、类型标注、abstract、import type 都能跑;enum、const enum、namespace、构造函数的参数属性会直接抛 ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX;再加一个 --experimental-transform-types 就能跑,但会打印实验特性警告。另外,它完全不做类型检查。
一、实测环境
node --version
# v24.9.0
node -p "process.platform + ' ' + process.arch"
# win32 x64
node -e "console.log(JSON.stringify(process.features.typescript))"
# "strip"
process.features.typescript 是本次实验里最直接的一行判据:默认值是 "strip"。加上 transform 开关后它会变成 "transform",这个后面再说。
测试目录里没有 package.json,所有文件都是散放在同一个文件夹里的 .ts。这一点会影响模块系统判定,但不影响下面关于语法的结论。文中所有结论都基于 v24.9.0 这一个版本,没有覆盖其他 24.x 小版本。
二、能直接跑的部分
第一个文件同时用了接口、类型别名、类型标注和 satisfies:
interface User { name: string; age: number }
type Id = string | number;
const u: User = { name: 'Ada', age: 36 };
const id = 42 satisfies Id;
function greet(x: User): string { return `hello ${x.name}`; }
console.log(greet(u), id);
$ node ok.ts
hello Ada 42
$ echo $?
0
这些写法的共同点是:类型信息只存在于源码里,运行时不产生任何代码。擦掉之后剩下的就是普通 JavaScript,所以 V8 照跑不误。
抽象类也能跑。注意抽象方法和普通方法不同——抽象方法只有声明,运行时不生成东西:
abstract class A { abstract go(): void; run(){ this.go(); } }
class B extends A { go(){ console.log('go'); } }
new B().run();
$ node abs.ts
go
类型导入同样没问题。下面这个文件同时有 import type 和普通的带类型导入,并且显式写了 .ts 后缀:
import type { Foo } from './types.ts';
import { v } from './mod.ts';
const x: Foo = { a: 1 };
console.log(x.a, v);
$ node imp.ts
1 7
内联的类型修饰符 import { type Foo } 也可以,实测输出 2,退出码 0。
三、会报错的四种写法
这四种写法的共同点是:TypeScript 编译器要为它们生成运行时代码——enum 要生成一个对象,namespace 要生成一个立即执行函数,参数属性要在构造函数里插入 this.name = name。只擦除、不生成,就做不出来,于是直接报错。
1. enum
enum Color { Red, Green }
console.log(Color.Green);
$ node enum.ts
enum Color { Red, Green }
^^^^^^^^^^^^^^^^^^^^^^^^^
SyntaxError [ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX]: TypeScript enum is not supported in strip-only mode
at parseTypeScript (node:internal/modules/typescript:68:40)
at processTypeScriptCode (node:internal/modules/typescript:146:42)
Node.js v24.9.0
$ echo $?
1
2. const enum
把 enum 换成 const enum,报错文案完全一样,仍是 TypeScript enum is not supported in strip-only mode,退出码 1。也就是说常量枚举并不能绕过这条限制。
3. namespace
namespace N { export const v = 1 }
console.log(N.v);
SyntaxError [ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX]: TypeScript namespace declaration is not supported in strip-only mode
4. 构造函数的参数属性
class P { constructor(public name: string) {} }
console.log(new P('x').name);
SyntaxError [ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX]: TypeScript parameter property is not supported in strip-only mode
这三种报错文案都带 ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX 错误码,退出码都是 1,并且 Node 会把出错的那一行和波浪线一起打出来,定位很直接。const enum 只是复用了 enum 那一条文案,所以本节算四类写法、三种报错文案。
四、加 --experimental-transform-types 就能跑
这四个文件换成同一条命令再跑:
$ node --experimental-transform-types enum.ts
1
$ node --experimental-transform-types ns.ts
1
$ node --experimental-transform-types ctor.ts
x
$ node --experimental-transform-types cenum.ts
1
全部退出码 0,结果也符合预期(Color.Green 是 1,N.v 是 1,new P('x').name 是 x)。代价是每次运行都会在 stderr 打印两行:
(node:10028) ExperimentalWarning: Transform Types is an experimental feature and might change at any time
(Use `node --trace-warnings ...` to show where the warning was created)
transform 模式下 process.features.typescript 返回 "transform"。另外 node --help 里能查到这两个相关开关:--experimental-transform-types 与 --no-experimental-strip-types。
五、模块解析:后缀必须写,tsconfig 的 paths 不生效
strip-only 不做类型检查,也不接管 TypeScript 的模块解析。最容易踩的是省略后缀:
import { v } from './mod';
console.log(v);
$ node nosuffix.ts
Error [ERR_MODULE_NOT_FOUND]: Cannot find module '<测试目录>\mod' imported from <测试目录>\nosuffix.ts
code: 'ERR_MODULE_NOT_FOUND'
(报错里的本机绝对路径已替换为 <测试目录>,其余逐字保留。)
必须有 .ts 后缀。同理,tsconfig.json 里的 baseUrl 和 paths 对 Node 无效。我用一份标准配置试了路径别名:
{"compilerOptions":{"baseUrl":".","paths":{"@lib/*":["./lib/*"]}}}
$ node app.ts
Error [ERR_MODULE_NOT_FOUND]: Cannot find package '@lib/util.ts' imported from <测试目录>\cfg\app.ts
code: 'ERR_MODULE_NOT_FOUND'
它把 @lib/util.ts 当成一个 npm 包名去找,而不会去读 tsconfig 里的映射。所以用 tsconfig paths 配了别名的现有项目,直接 node app.ts 是跑不起来的,要么改相对路径加 .ts,要么继续用 tsc 或打包工具。
反过来,纯类型导入连解析都不做。下面这行从一个根本不存在、也不带后缀的文件里导入类型:
import type { Foo } from './types-noext';
const x: Foo = { a: 1 };
console.log(x.a);
$ node typeonly.ts
1
语句被整行擦掉了,文件根本没去找。这也是为什么"只导入类型"不会引发模块解析错误——但写错名字也不会有人提醒你。
还有一个容易被忽略的限制:node_modules 下的 .ts 不处理
如果某个依赖直接把 .ts 当入口发布,Node 不会去擦它的类型。我把一个带 "main": "index.ts" 的包放进 node_modules 再导入,得到的是:
Error [ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING]: Stripping types is currently unsupported for files under node_modules, for "<测试目录>\node_modules\pkg\index.ts"
加上 --experimental-transform-types 也一样报这个错。结论很明确:node_modules 下的包仍然应该是编译后的 JavaScript,原生跑 .ts 只适用于自己项目里的源码。
六、不做类型检查,也不管你写错什么
这条要单独强调。下面这段把字符串赋给了 number 变量,函数签名也声称返回 number,实际返回的是字符串:
const n: number = "not a number";
function f(x: string): number { return x; }
console.log(n, f("abc"));
$ node typecheck.ts
not a number abc
$ echo $?
0
没有任何警告,退出码 0。同一份文件交给 tsc --noEmit 检查(本机 TypeScript 7.0.2),赋值和返回值各报一条错误,两条都是 TS2322: Type 'string' is not assignable to type 'number'。类型检查仍然要交给 tsc --noEmit、编辑器的语言服务或 CI。Node 的运行时只保证"能擦的擦干净、擦不掉的报错"。
七、CommonJS 照旧可用
.ts 文件里写 require 也能跑,实测 typeof os.platform() 输出 string,退出码 0。判断模块类型看最近的 package.json:没有 type 字段时 Node 会检测源码里的 import/export,有就按 ESM 处理;一旦写了 "type": "commonjs",.ts 里的 import 就不再被当成 ESM,实测直接报:
(node:35580) Warning: Failed to load the ES module: <测试目录>\esm.ts. Make sure to set "type": "module" in the nearest package.json file or use the .mjs extension.
SyntaxError: Cannot use import statement outside a module
八、把开关关掉会怎样
$ node --no-experimental-strip-types ok.ts
interface User { name: string; age: number }
^^^^
SyntaxError: Unexpected identifier 'User'
关掉之后 Node 就完全不认 TypeScript,连接口声明都当语法错误。这个开关主要用于排查"到底是 Node 在处理类型,还是别的工具在处理"。
九、实用建议
基于上面这些实测,我的用法是:
1. 单文件脚本、小工具:直接 node x.ts,能省掉一次编译。
2. 代码里出现 enum / namespace / 参数属性:要么改写成等价 JS
(对象常量 + as const 代替 enum,普通类字段赋值代替参数属性),
要么加 --experimental-transform-types 并接受实验警告。
3. 相对导入一律带 .ts 后缀;路径别名留在打包工具或 tsc 里。
4. 类型检查单独跑 tsc --noEmit,不要指望 node 帮你发现问题。
十、实测结果汇总
| 写法 | 默认 strip-only | 加 --experimental-transform-types |
|---|---|---|
| interface / type / 类型标注 / satisfies | 通过,输出 hello Ada 42 | 通过 |
| abstract class | 通过,输出 go | 通过 |
| import type / import { type X } | 通过 | 通过 |
| enum | ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX | 通过,输出 1 |
| const enum | ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX | 通过,输出 1 |
| namespace | ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX | 通过,输出 1 |
| 构造函数参数属性 | ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX | 通过,输出 x |
| 省略 .ts 后缀的导入 | ERR_MODULE_NOT_FOUND | 同样报错 |
| tsconfig paths 别名 | ERR_MODULE_NOT_FOUND | 同样报错 |
| .ts 里写 require | 通过,输出 string | 通过 |
"type": "commonjs" 下的 import | SyntaxError: Cannot use import statement outside a module | 同样报错 |
| node_modules 里包的 .ts 入口 | ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING | 同样报错 |
| 类型写错 / 返回类型不符 | 静默通过,不检查 | 同样不检查 |
一句话总结:Node 24 原生跑 TypeScript 解决的是"少一步编译",不是"替代 tsc"。理解 strip-only 的边界——擦类型可以,生成运行时结构不行——能省掉大部分意外报错。
- 点赞
- 收藏
- 关注作者
评论(0)