TypeScript 5.8 beta 版本来了,你可以在
5.8 Iteration Plan
查看所有被包含的 Issue 与 PR。如果想要抢先体验新特性,执行:
$ npm install typescript@beta
来安装 beta 版本的 TypeScript,或在 VS Code 中安装
JavaScript and TypeScript Nightly
,并选择为项目使用 VS Code 的 TypeScript 版本(cmd + shift + p, 输入 select typescript version),来更新内置的 TypeScript 支持。
函数类型推导优化
在 TypeScript 中,我们经常遇到函数声明的返回值类型依赖于参数类型的情况,如以下的示例:
enum ValueKind {
Single,
Multiple,
}
async function getValue(
value: ValueKind,
): Promise<string | string[]> {
if (value === ValueKind.Single) {
return '';
}
else {
return [];
}
}
我们希望,getValue 的返回值类型取决于参数 value 的类型。这在逻辑层是很好实现的,但在类型层面则需要一些额外的处理,比如你可能会首先想到重载:
async function getValue(
value: ValueKind.Single,
): Promise<string>;
asyncfunction getValue(
value: ValueKind.Multiple,
): Promise<string[]>;
asyncfunction getValue(
value: ValueKind,
): Promise<string | string[]> {
if (value === ValueKind.Single) {
return'';
}
else {
return [];
}
}
getValue(ValueKind.Single) // string;
getValue(ValueKind.Multiple) // string[];
然而重载有两个比较显著的问题,首先,它并不会在函数实现内检查每个重载实现的返回值,比如:
if (value === ValueKind.Single) {
- return '';
+ return [];
}
这里的返回值实际上并不符合我们指定的重载声明,但 TypeScript 目前并不会检查其有效性。
另外一个问题则是,在参数类型-返回值类型对应较多的情况下,密密麻麻的重载会导致函数签名难以阅读,同时也不能很好处理参数类型-返回值类型对应关系需要通过进一步计算得到,而不是直接枚举的情况。
另外一个实现这种情况的方式则是使用条件类型:
type ReturnValueextends ValueKind> =
S extends ValueKind.Multiple ? string[] :
string;
async function getValue<S extends ValueKind>(
value: S,
): Promise<ReturnValue<S>> {
if (value === ValueKind.Single) {
return ''; // Type 'string' is not assignable to type 'ReturnValue'
}
else {
return []; // Type 'string[]' is not assignable to type 'ReturnValue'
}
}
但这种使用也会带来新的问题,即如上的报错。这是因为 TypeScript 会直接将返回语句的类型,与函数返回值的类型进行比较,即 string vs
S extends ValueKind.Multiple ? string[] : string
,而在这个比较过程中,类型参数 S 是还没有被填充的,因此这个类型比较必定不兼容——类似于重载的情况。
你当然可以通过额外的类型断言来修正这一情况,但这样就变成是我们在指导 TypeScript 类型,而不是由它自己来推导类型并确保类型安全了。
很容易想到,既然我们都已经走到能够进行类型收窄的语句内了,如果能对应填充下参数类型,岂不是就解决这个问题了?在 TypeScript 4.8 版本中就支持了这一能力:
type ReturnValueextends ValueKind> =
S extends ValueKind.Multiple ? string[] :
S extends ValueKind.Single ? string :
never;
asyncfunction getValue<S extends ValueKind>(
value: S,
): Promise<ReturnValue<S>> {
if (value === ValueKind.Single) {
return'';
}
else {
return [];
}
}
要使用这个能力,我们需要更新一下 ReturnValue 工具类型的实现,让我们预期的所有参数类型的匹配结果都显式标注出来(语句块内对条件类型进行类型收窄时,只会在完全命中一个 extends 条件时生效)。
require ESM 支持
说实话我每次看到 CJS 和 ESM 的『你中有我,我中有你』时都感到一丝蛋疼,估计 Ryan 也没想到这么多年过去了,社区还在打补丁,就好像在努力挽回一段破碎的婚姻一样。
此前我们可以在 ESM 中加载 CJS 模块,但反之则行不通,所以当你必须兼容 CJS 时,又遇到只发布了 ESM 产物的 npm 包,那得是多么头大的情况。而 NodeJS 22 版本实验性质地支持了使用 require 加载 ESM 文件(
loading-ecmascript-modules-using-require
),能够在 CJS 中加载同步(即不包括 top-level await)的 ESM 文件。
TS 5.8 版本现在能够在启用 --module nodenext 配置时支持这一行为,需要注意的是由于这个功能特性可能会反向移植到更早的 NodeJs 版本,比如 Node 20,因此还不会提供具体的如 --module node18 这样精确的版本控制。
NodeJs TS 支持 --erasableSyntaxOnly
NodeJs 22.6.0 版本开始通过实验性的
--experimental-strip-types
选项支持直接执行 TS 文件,同时在 23.6.0 版本开始默认启用了此配置,也就是说,你可以直接 node index.ts 而不需要额外的命令行参数了,对应的,23.6.0 也新增了
--no-experimental-strip-types
配置来阻止这一默认行为。
NodeJs 底层是通过把类型相关的代码替换为空白字符串来实现此能力的,其底层是
Amaro
,Amaro 底层则是
@swc/wasm-typescript
,整体使用大致是这样:
const amaro = require('amaro');
const { code } = amaro.transformSync("const foo: string = 'bar';", { mode: "strip-only" });
console.log(code); // "const foo = 'bar';"
对于某些不能直接移除的 TS 特性(比如枚举),NodeJs 23.7.0 也支持了
--experimental-transform-types
配置来做执行前的语法转换(通过设置 transformSync options 的 mode 为
'transform'),比如第一个例子中的代码,如果直接执行会有这么个错误:
node:internal/modules/typescript:67
throw new ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX(error.message);
^
SyntaxError [ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX]: x TypeScript enum is not supported in strip-only mode
,-[1:1]
1 | ,-> enum ValueKind {
2 | | Single,
3 | | Multiple,
4 | `-> }
如果你在迁移某些工具链到原生的 NodeJs TS 支持,并且希望使用 'strip-only' 模式,那么就必须检查出代码中无法直接 strip 的部分。因此,TypeScript 5.8 引入了
--erasableSyntaxOnly
配置,能够帮你提前检查出这种代码。
先是为 JS 引入类型注释的 TC39 提案
proposal-type-annotations
,接着是 NodeJs 支持直接执行 TS 文件,感觉浏览器直接运行 TS 文件也不远了?
变量初始化检查(5.7 补档)
TS 5.7 版本开始支持了对跨函数上下文的未初始化变量检查,如对于以下的例子:
function foo() {
let result: number
function printResult() {
console.log(result); // ok before 5.7, error since 5.7
}
}
TS 5.7 版本只有这个类型能力增强值得一写,所以我就直接把 TS 5.7 的 Devblog 鸽了 :-)
其它
计算属性类型声明优化
此前 TypeScript 会约束 Class 结构内的计算属性,要求其类型必须是字面量类型或 symbol 类型,如: