欢迎关注公众号『
JavaScript与编程艺术』高质量文章优先公众号发布。
Node.js V22.7.0 无需编译可以直接运行 TS 文件,可以加速我们的开发过程,这是伟大的进步。原理是运行时将类型剥离(strip-types 类型被等量空格替代,非常巧妙的方式后文会讲到为何巧妙),但并不支持所有 TS 类型或特性,比如需要编译的就不会支持比如 enum,原因详见下文 roadmap。
执行 ts 有多种运行时,本文采用最快的
bun作为对比。如何最快速最节约磁盘安装 bun 详见 一个专业的前端如何在国内安装
bun]。
Node.js 执行 ts 文件需要增加一个实验性的 flag --experimental-strip-types,Node.js 🟢 v22.18.0 无需因为已稳定。
错误姿势 🚫:
node index.ts --experimental-strip-types
正确姿势 ✔️:
node --experimental-strip-types index.ts
1. 支持简单类型
function greet(name: string) {
return `Hello, ${name}!`;
}
console.log(greet('World'));
效果:
❯ bun index.ts && node --experimental-strip-types index.ts
Hello, World!
Hello, World!
2. 支持类型别名 type
function count(list: any[]): int {
return list.length;
}
console.log(
'There are', count(['James']), 'items in the list'
);
type int = number;
效果:
❯ bun index.ts && node --experimental-strip-types index.ts
There are 1 items in the list
There are 1 items in the list
3. 支持 interface
interface Info {
girlFriends: string[];
}
function introduce(name: string, { girlFriends }: Info): number {
console.log(name, 'has', girlFriends.length, 'girlFriends:', girlFriends);
}
introduce('James', {
girlFriends: ['Mary', 'Margaret', 'Martha', 'Marie', 'Marion'],
});
效果:
❯ bun index.ts && node --experimental-strip-types index.ts
James has 5 girlFriends: [ "Mary", "Margaret", "Martha", "Marie", "Marion" ]
James has 5 girlFriends: [ 'Mary', 'Margaret', 'Martha', 'Marie', 'Marion' ]
4. 支持泛型
function count<T extends any>(list: T[]): int {
return list.length;
}
console.log('There are', count(['James']), 'items in the list');
type int = number;
效果:
❯ bun index.ts && node --experimental-strip-types index.ts
There are 1 items in the list
There are 1 items in the list
5. 支持类型体操 和 as
即类型之间的交互和转换生成新的类型。因为本质上这些类型都可以被擦除,故可以支持。
type ExtractK<
T extends string,
Keys extends any[] = []
> = T extends `${string}{{${infer K}}${infer T}`
? ExtractK<T, [...Keys, K]>
: Keys;
type t1 = ExtractK<'hello {{foo}} {{bar}}'>; // ['foo', 'bar']
type ArrToInterface<A extends string[]> = { [P in A[number]]: string };
type t2 = ArrToInterface<t1>; // { foo: string; bar: string }
function extractKeys<T extends string>(str: T): ArrToInterface<ExtractK<T>> {
return { foo: str, bar: str } as ArrToInterface<ExtractK<T>>;
}
const baz = extractKeys('hello {{foo}} {{bar}}');
console.log('baz:', baz);
效果:
❯ bun index.ts && node --experimental-strip-types index.ts
baz: {
foo: "hello {{foo}} {{bar}}",
bar: "hello {{foo}} {{bar}}",
}
baz: { foo: 'hello {{foo}} {{bar}}', bar: 'hello {{foo}} {{bar}}' }
可以看到 bun 对输出更加友好。
6. 不支持 enum
enum Gender {
Male = 'male',
Female = 'female',
}
function introduce(name: string, gender: Gender): void {
console.log(name, 'is', gender);
}
introduce('James', Gender.Male);
效果:
❯ bun index.ts
James is male
❯ node --experimental-strip-types index.ts
node:internal/deps/amaro/dist/index:226
throw takeObject(r1);
^
x TypeScript enum is not supported in strip-only mode
,-[73:1]
70 | // girlFriends: ['Mary', 'Margaret', 'Martha', 'Marie', 'Marion', ]
71 | // }))
72 |
73 | ,-> enum Gender {
74 | | Male = 'male',
75 | | Female = 'female',
76 | `-> }
77 |
78 | function introduce(name: string, gender: Gender): void {
79 | console.log(name, 'is', gender);
`----
(Use `node --trace-uncaught ...` to show where the exception was thrown)
Node.js v22.7.0
可以看到关键词 strip-only mode,后续应该不会有 compile-mode(后文 roadmap 会提及)。
为什么不支持 enum,因为 enum 不仅仅是类型,运行时会存在,比如
enum Gender {
Male = 'male',
Female = 'female',
}
被 TS 编译成:
var Gender;
(function (Gender) {
Gender["Male"] = "male";
Gender["Female"] = "female";
})(Gender || (Gender = {}));
其次 const num 也不支持,因为同样需要编译(只是编译成 inline 形式):
function introduce(name, gender) {
console.log(name, 'is', gender);
}
introduce('James', "male" /* Gender.Male */);
为何需要编译的不支持,详见下文。
小结
优点
- 无编译,加快开发速度
- 无需将类型写到 tsdoc 注释中或者
.d.ts,类型变成一等公民 - 支持大部分类型
不足或者目前的限制
- 需要转换的,或运行时需要留痕的不支持(enum、decorator、namespace……)
- 当然类型检查是没有的,roadmap 也不会支持。
.js文件不支持类型。- node_modules 里面的 ts 不会运行。
- 没有 sourcemap 也无需。
解读
需转换的特性不支持
enum 等需要转换的特性不支持的原因:转换意味着需要 sourcemap 这是很大的工作量、其次支持越多更新越快意味着 breaking change 也会更多,对 Node.js 本身稳定性存在负面影响。
enum 的 workaround,我们可以使用对象 + as const 来实现 enum,之前的示例代码可以改写成如下:
// mimic enum use plain object and `as const`
const Gender = {
Male: 'male',
Female: 'female',
} as const;
type Gender = (typeof Gender)[keyof typeof Gender];
function introduce(name: string, gender: Gender): void {
console.log(name, 'is', gender);
}
introduce('James', Gender.Male);
不支持运行 node_modules 里的 ts 文件
当前不支持是为了避免出现仅有 TS 的 npm 包。估计是还不够成熟。
为什么无需 sourcemap
因为会用等量空格或空行替代类型,源码行数保持不变。
我们用 Nodejs 使用的类型擦除包验证下。包 amaro 的介绍和为何出现下文会讲到。
const amaro = require('amaro');
const codeWithTypes = "const foo: string = 'bar';";
const { code } = amaro.transformSync(codeWithTypes, {
mode: 'strip-only',
});
console.log(code);
console.log(codeWithTypes);
// "const foo = 'bar';"
// “const foo: string = 'bar';”
执行结果:
❯ node amaro-test.js
const foo = 'bar';
const foo: string = 'bar';
可以看到类型被等量空格替代,保持了源码的位置故无需 sourcemap。我们再来看一个稍微复杂的例子:
function count<T extends any>(list: T[]): int {
return list.length;
}
type int = number;
console.log('There are', count(['James']), 'items in the list');
被 amaro 转换后的代码:
function count (list ) {
return list.length;
}
console.log('There are', count(['James']), 'items in the list');
可以看到内联的类型被等量空格替换,包括泛型 <T extends any>、入参 : T[]、返回值类型 : int;独立的类型比如类型别名 type int = number; 被空行替换。转换前后行数都是 7 行,而且代码行列号都保持了和源码一模一样,故无需 sourcemap。
小结
虽然存在这些不足,但原生支持类型后确实很方便,无需编译(我最不想要的就是编译!),我日常写代码也一定会写类型,所以这对我帮助很大。我的开发模式:mjs + tsdoc 或 mjs + index.d.ts,这是我最近写 node.js 工具的标配,感兴趣的同学可以看看 swaggered ~ github 或 ydd,类型少则写到注释中,否则写到 .d.ts。
其次类型检查不支持也不是致命缺点,其实也无需,编辑器可以帮我们做。
社区担心
- 担心会和 TS 割裂,导致兼容性问题。为什么 deno 不会有这个担忧?
- Node.js 的版本更新会被 TS 绑架。TS 有新语法必须跟进,会阻碍对 LTS 的演进
“不可阻挡的光明的”未来
虽然有这么多担忧,但是 JS 原生支持类型大势所趋,浏览器支持类型已经有了提案 TC39 Proposal for Type Annotations。TypeScript 的核心成员和 Node.js 的开发者也都在着手解决这些担忧,有条件的可以看看 YouTube 视频 meeting on 24th of July。JS 和 TS 的鸿沟相信会被顺利抹平,所以未来仍然是“不可阻挡的光明的”。
Node.js 类型 Roadmap
- ✅ 第 1 步:通过简单的类型剥离让 ts 文件跑起来
- ✅ 第 2 步:解耦。将核心功能发布到 npm,已发布即包 amaro
- 第 3 步:性能提升
- 第 4 步:增加更多功能
amaro 介绍
- 让 Node.js 支持类型的核心包,定位是 TypeScript transpiler。通过将其独立成包,可以和 Node.js 解耦达到新功能可以快速开发和升级,以及有最新特性如果当前 Node.js 版本不支持,可以单独下载最新的 amaro 包。
- 使用 swc 实现。因为 @swc/wasm-typescript 简单、经过充分验证 Deno 也是使用 swc。未来可能在 Native 层实现。
- 该包目前仅支持 strip-mode 也就是如果我们前例中运行 enum 代码报错里面提到过。后续可能支持更多模式。
- 后续可能支持 tsconfig。