好久没有写博客了,也由于工作变动的原因,好久没有关注 TypeScript 的 Release Note 了, 今天趁着项目空档摸鱼的间隙好好来学习下。
类型谓词 (type predicates)
该特性存在已久,但在 5.5 版本中,得到了增强,在特定的情况下可以省略类型谓词而达到同样的效果。
// 以前需要这么写:
const isNumber = (x: unknown): x is number => typeof x === 'number';
// 现在可以省略类型谓词
const isNumber = (x: unknown) => typeof x === 'number';
当然,自动推断类型谓词需要符合如下条件:
- The function does not have an explicit return type or type predicate annotation.
- The function has a single return statement and no implicit returns.
- The function does not mutate its parameter.
- The function returns a boolean expression that’s tied to a refinement on the parameter.
如下是一个不能自动推断的反例:
[0, 1, 2, 3, null, 5].filter(x => !!x).map(i => i.toFixed());
// 'i' is possibly 'null'.
违背了上述的第二个条件:
TypeScript did not infer a type predicate for x => !!x, and rightly so: if this returns true then x is a number. But if it returns false, then x could be either undefined or a number (specifically, 0). This is a real bug: if any student got a zero on the test, then filtering out their score will skew the average upwards. Fewer will be above average and more will be sad!
闭包与类型缩窄
在之前的版本中,类型缩窄后,在作用域内的闭包函数内是不生效的,看例子:
function getUrls(url: string | URL, names: string[]) {
if (typeof url === "string") {
url = new URL(url);
}
return names.map(name => {
url.searchParams.set("name", name)
// ~~~~~~~~~~~~
// error!
// Property 'searchParams' does not exist on type 'string | URL'.
return url.toString();
});
}
这在早期是可以理解的,因为回调函数的调用时机不可预测,这就导致哪怕前面已经通过 if 判断缩窄了 url
的类型到 URL
,但无法保证在回调函数被调用时,其它地方不会将 url
修改为 string。
在 5.4 版本中,如果在作用域范围内,没有对 url
重新赋值其它类型,则认为在回调的闭包内依旧是缩窄后的类型:
function getUrls(url: string | URL, names: string[]) {
if (typeof url === "string") {
url = new URL(url);
}
// url = 'xxx'; 将会导致类型缩窄失效
// setTimeout(() => url = 'xxx'); 也会导致类型缩窄失效
return names.map(name => {
// 否则,url 就是 URL 类型
url.searchParams.set("name", name)
return url.toString();
});
}
NoInfer
Release Note 中的例子很贴切:
function createStreetLight<C extends string>(colors: C[], defaultColor?: C) {
// ...
}
// 我们预期是这样: defaultColor 应该是 colors 中的其中一个
createStreetLight(["red", "yellow", "green"], "red");
// 但实际上,这里 C 被推断成了 "red" | "yellow" | "green" | "blue"
createStreetLight(["red", "yellow", "green"], "blue");
// 为了处理这种情况,一般会新增第二个泛型参数来约束 defaultColor
function createStreetLight<C extends string, D extends C>(colors: C[], defaultColor?: D) {
}
createStreetLight(["red", "yellow", "green"], "blue");
// ~~~~~~
// error!
// Argument of type '"blue"' is not assignable to parameter of type '"red" | "yellow" | "green" | undefined'.
在 5.4 之后,新增了 NoInfer Utility type 后就不需要第二个泛型参数了,可以直接这样:
function createStreetLight<C extends string>(colors: C[], defaultColor?: NoInfer<C>) {
// ...
}
createStreetLight(["red", "yellow", "green"], "blue");
// ~~~~~~
// error!
// Argument of type '"blue"' is not assignable to parameter of type '"red" | "yellow" | "green" | undefined'.