今天,我们很高兴地宣布 TypeScript 4.4 候选版本(RC)已经到来!因此从现在起到 TypeScript 4.4 稳定版,除了对关键 bug 做出修复之外,预计不会再有其他更深层次的变化调整。如果你想现在就尝试 TypeScript 的 RC 版,可以通过 NuGet 获取,或者使用以下 npm 命令:
npm install typescript@rc
TypeScript 4.4 版本中的部分主要亮点包括别名条件与判别式的控制流分析、符号与模板字符串模式索引签名、性能改进、JavaScript 拼写建议等。
下面就来一起看看吧!
在 JavaScript 当中,我们往往需要以不同的方式探测同一变量,查看它是否有我们可以使用的具体类型。TypeScript 能够理解这些探测操作,并将其设定为类型守卫(type guard)。类型检查器会使用“控制流分析”机制推断每个语言构造中的类型,这就省去了在使用时对 TypeScript 变量类型做出声明的麻烦。
例如,我们可以写出如下代码形式:
function foo(arg: unknown) {
if (typeof arg === "string") {
// 现在我们知道这是一条字符串。
console.log(arg.toUpperCase());
}
}
在此示例中,我们会检查 arg 是否是一条 string。TypeScript 识别出了 typeof arg === "string" 检查,将其理解为类型守卫,并能够判断出 arg 应该是 if 块主体中的 string。
但是,如果我们把条件变更为常量,结果又将如何?
function foo(arg: unknown) {
const argIsString = typeof arg === "string";
if (argIsString) {
console.log(arg.toUpperCase());
// ~~~~~~~~~~~
// 错误!类型「unknown」上不存在属性「toUpperCase」。
}
}
在以往的 TypeScript 版本中,这会触发一项错误——即使 argIsString 被分配到了类型守卫值,TypeScript 也只会丢失该信息。这不科学,毕竟用户很可能希望在多个位置重复执行相同的检查。为了解决这个问题,之前大家只能重复操作或者使用类型断言(强制转换)。
但在 TypeScript 4.4 中,问题已不复存在。以上示例不会引发任何错误!当 TypeScript 发现我们在测试某个常量值时,它会执行一些额外的操作以查看其中是否包含类型守卫。如果该类型守卫对 const、readonly 属性或者未修改的参数执行操作,则 TypeScript 能够适当缩小该值。
除 typeof 检查之外,TypeScript 还提供多种不同的类型守卫条件。例如,对 charm 等可区分联合进行检查。
type Shape =
| { kind: "circle", radius: number }
| { kind: "square", sideLength: number };
function area(shape: Shape): number {
const isCircle = shape.kind === "circle";
if (isCircle) {
// 我们知道这里有个圆!
return Math.PI * shape.radius ** 2;
}
else {
// 我们知道这里有个正方形!
return shape.sideLength ** 2;
}
}
4.4 版本对于判别式的分析也更为深入——现在,大家可以提取出判别式,而 TypeScript 则能够缩小原始对象的范围。
type Shape =
| { kind: "circle", radius: number }
| { kind: "square", sideLength: number };
function area(shape: Shape): number {
// 首先提取出「kind」字段。
const { kind } = shape;
if (kind === "circle") {
// 我们知道这里有个圆!
return Math.PI * shape.radius ** 2;
}
else {
// 我们知道这里有个正方形!
return shape.sideLength ** 2;
}
}
再举一例,以下函数用于检查两个输入中是否有内容。
function doSomeChecks(
inputA: string | undefined,
inputB: string | undefined,
shouldDoExtraWork: boolean,
) {
let mustDoWork = inputA && inputB && shouldDoExtraWork;
if (mustDoWork) {
// 能够访问'inputA'与'inputB'上的「string」属性!
const upperA = inputA.toUpperCase();
const upperB = inputB.toUpperCase();
// ...
}
}
TypeScript 能够理解在 mustDoWork 为 true 的情况下,inputA 与 inputB 都存在。这意味着我们用不着再编写像 inputA! 这样的非空断言来向 TypeScript 强调 inputA 并非 undefined。
更奇妙的是,这种分析机制是可以传递的。如果我们将某个常量分配给某个包含多个常量的条件,而且各个常量都被分配到了类型守卫,那么 TypeScript 随后即可传递这些条件。
function f(x: string | number | boolean) {
const isString = typeof x === "string";
const isNumber = typeof x === "number";
const isStringOrNumber = isString || isNumber;
if (isStringOrNumber) {
x; // 'x'的类型为'string | number'。
}
else {
x; // 'x'的类型为'boolean'。
}
}
请注意,新机制的深度是有极限的——TypeScript 在检查这些条件时不会过度深入,但对大多数日常检查来说应该是足够了。
这项功能应该会让更多 JavaScript 代码能够直接在 TypeScript 中“正常起效”。关于更多详细信息,请参阅 GitHub 上的实现。
链接:https://github.com/microsoft/TypeScript/pull/44730
TypeScript 允许大家使用索引签名来描述各个属性都必须具备的特定对象。如此一来,我们就能将这些对象作为类似于字典的类型,并在其中通过中括号使用字符串键对它们进行索引。
例如,我们可以编写一个带有索引签名的类型,此类型接收 string 键并映射为相应的 boolean 值。如果我们尝试分配 boolean 值以外的值,则返回错误。
interface BooleanDictionary {
[key: string]: boolean;
}
declare let myDict: BooleanDictionary;
// 分配 boolean 值有效
myDict["foo"] = true;
myDict["bar"] = false;
// 错误,"oops"不是 boolean 值
myDict["baz"] = "oops";
虽然这里使用 Map 数据结构可能更好(即 Map<string, boolean>),但这里考虑的是 JavaScript 对象的易用性更强、或者是项目恰好这么要求。
同样的,Array
// 这里是 TypeScript 内置 Array 类型定义的一部分。
interface Array<T> {
[index: number]: T;
// ...
}
let arr = new Array<string>();
// 有效
arr[0] = "hello!";
// 错误,这里需要一个「string」值
arr[1] = 123;
索引签名特别适用于在外部表达大量代码的情况;但到目前为止,索引签名仅适用于 string 及 number 键(而且 string 索引中还故意设置一项特性,即可以接受 number 键,这是因为数字键总会被强制转换为字符串)。换句话说,TypeScript 不允许使用 symbol 键作为索引对象。TypeScript 也无法对某些 string 键子集的索引签名进行建模——例如用于描述一切以文本 data- 作为名称开头的属性的索引签名。
TypeScript 4.4 解决了上述限制,已经将索引签名的适用范围拓展到符号与模板字符串模式当中。
例如,TypeScript 现在允许用户声明采用任意 symbol 键的类型。
interface Colors {
[sym: symbol]: number;
}
const red = Symbol("red");
const green = Symbol("green");
const blue = Symbol("blue");
let colors: Colors = {};
colors[red] = 255; // 允许赋值
let redVal = colors[red]; // 'redVal'的类型为'number'
colors[blue] = "da ba dee"; // 错误:类型'string'无法分配给类型'number'。
同样的,我们也可以使用模板客串模式类型编写索引签名。这种作法常见于筛选操作,例如在 TypeScript 的多余属性检查中剔除一切以 data- 开头的属性。当我们将对象字面量传递给具有预期类型的内容时,TypeScript 即可检查未在预期类型中得到声明的多余属性。
interface Options {
width?: number;
height?: number;
}
let a: Options = {
width: 100,
height: 100,
"data-blah": true, // 错误!「data-blah」未在「Options」中声明。
};
interface OptionsWithDataProps extends Options {
// 允许任何以'data-'开头的属性。
[optName: `data-${string}`]: unknown;
}
let b: OptionsWithDataProps = {
width: 100,
height: 100,
"data-blah": true, // 成功了!
"unknown-property": true, // 错误!'unknown-property' 未在'OptionsWithDataProps'中声明。
};
关于索引签名的最后一项要点是,其现在可以支持无限域原始类型的联合,具体包括:
-
string
-
number
-
symbol
-
模板字符串模式 (例如
hello-${string}
)
参数为这些类型的联合的索引签名将脱糖为几个不同的索引签名。
interface Data {
[optName: string | symbol]: any;
}
// 等价于
interface Data {
[optName: string]: any;
[optName: symbol]: any;
}
在 JavaScript 中,任何值的类型都可使用 throw 抛出并在 catch 子句中进行捕捉。因此,TypeScript 以往一直将 catch 子句变量类型化为 any,且不允许任何其他类型注释:
try {
// 谁知道这会抛出什么...
executeSomeThirdPartyCode();
}
catch (err) { // err: any
console.error(err.message); // 允许,因为符合'any'
err.thisWillProbablyFail(); // 允许,因为符合'any' :(
}
这一次,TypeScript 迎来了 unknown 类型;对于需要尽可能提高正确性与类型安全性的用户来说,unknown 在 catch 子句中显然要比 any 更好,因为它可以更好地缩小范围并迫使我们针对任意值做出测试。最终,TypeScript 4.0 版本开始允许用户在各个 catch 子句变量上指定 unknown (或者 any) 的显式类型注释,以便根据具体情况选择更严格的类型;但对很多开发者来说,在每一个 catch 子句上手动指定: unknown 确实非常麻烦。
因此,TypeScript 4.4 引入了一个名为 --useUnknownInCatchVariables 的新标记。此标记能够将 catch 子句变量的默认类型由 any 变更为 unknown。
try {
executeSomeThirdPartyCode();
}
catch (err) { // err: unknown
// 错误!类型'unknown'上不存在'message'。
console.error(err.message);
// 成功了!我们可以将'err'由'unknown'缩小为'Error'。
if (err instanceof Error) {
console.error(err.message);
}
}
此标记归属于 --strict 选项系列。所以如果您使用 --strict 检查代码,此选项将自动开启。但您也可能在 TypeScript 4.4 上遇到如下错误:
类型'unknown'上不存在属性'message'。
类型'unknown'上不存在属性'name'。
类型'unknown'上不存在属性'stack'。
如果我们不想在 catch 子句中处理 unknown 变量,则可以始终添加明确的 : any 注释以声明不使用更严格的类型。
try {
executeSomeThirdPartyCode();
}
catch (err: any) {
console.error(err.message); // 再次成功!
}
在 JavaScript 当中,读取对象上的属性缺失会产生 undefined 值。当然,也可能有某些实际属性的值确实为 undefined。JavaScript 中的很多代码都倾向于相同的方式处理这些情况,所以以其为基础的 TypeScript 最初也只是解释每个可选属性,类似于用户在类型中写入了 undefined。例如:
interface Person {
name: string,
age?: number;
}
被认为等价于
interface Person {
name: string,
age?: number | undefined;
}
这意味着用户可以明确使用 undefined 代替 age。
const p: Person = {
name: "Daniel",
age: undefined, // 默认情况下没有问题。
};
因此,TypeScript 在默认情况下并不能区分实际值为 undefined 的属性与缺失的属性。虽然大多数情况下这并不是什么问题,但也有一些 JavaScript 代码会做出不同的假设。Object.assign, Object.keys, object spread ({ ...obj }) 以及 for–in 循环等函数及运算符的行为都取决于对象之上是否实际存在属性。在我们的 Person 示例中,如果 age 属性出现在很重要的上下文信息当中,则很可能引导运行时错误。
在 TypeScript 4.4 中,新的标记 –exactOptionalPropertyTypes 负责强调完全按字面形式解释各个可选属性类型,也就是说 | undefined 不会被添加至类型当中:
// 当启用'exactOptionalPropertyTypes'时:
const p: Person = {
name: "Daniel",
age: undefined, // 错误!undefined 不是数字
};
此标记并不属于 --strict 系列,所以如果需要这种功能,请明确将其启用。另外,它还要求启用 --strictNullChecks。我们将陆续更新 DefinitelyTyped 与其他更多定义,尽可能帮助大家降低转换难度;当然,根据实际代码结构的不同,您也可能会遇到某些具体问题。
TypeScript 4.4 还支持在类中使用 static 块。这是一项即将推出的 ECMAScript 功能,可帮助您为静态成员编写出更复杂的初始化代码。
class Foo {
static count = 0;
// 此为 static 块:
static {
if (someCondition()) {
count++;
}
}
}
这些 static 块允许您编写具有自身范围的语句序列,由这些语句访问包含类之内的私有字段。换句话说,我们能够编写出具备所编写语句全部功能的初始化代码,可以在完全访问类内容的同时不致泄露变量。
class Foo {
static #count = 0;
get count() {
return this.#count;
}
static {
try {
const lastInstances = loadLastInstances();
count += lastInstances.length;
}
catch {}
}
}
如果没有 static 块,我们也可以使用上述代码,但会在不同的类型里留下安全隐患。
请注意,同一个类可以包含多个 static 块,各个块的运行顺序等同于其编写顺序。
// Prints:
// 1
// 2
// 3
class Foo {
static prop = 1
static {
console.log(1);
}
static {
console.log(2);
}
static {
console.log(3);
}
}
TypeScript 的 --help 选项已经迎来更新!感谢 Song Gao 的辛勤工作,我们成功调整并更新了编译器选项的描述,并使用颜色及其他视觉元素重新设计了 --help 菜单的样式。目前我们仍在对设计样式进行迭代,希望默认主题能在各个平台上正常工作,大家也可以参考原始提案了解新菜单的基本外观。
https://github.com/microsoft/TypeScript/issues/44074
声明发布速度更快
TypeScript 正在考量内部符号能否在不同上下文中访问,以及应如何打印特定类型。这些变量有望提高 TypeScript 在高复杂度代码中的整体性能,特别是在使用 --declaration 标记的.d.ts 文件发布场景之下。
路径归一化速度更快
TypeScript 往往需要对各种文件路径类型进行“归一化”,确保将其转换为编译器能够随处使用的统一格式。具体操作包括使用斜杠来替换反斜杠,或者删除路径中的 /./ 以及 /../ 等等。但在处理包含数百万条路径的庞大项目时,这类操作终究会拖慢工作进度。所以 TypeScript 4.4 会首先对路径进行快速检查,查看其是否需要进行归一化处理。这项改进将大型项目的加载时长缩短了 5% 到 10%;我们在内部对大型项目进行测试时,发现加载时间确实明显改善。
路径映射速度更快
TypeScript 希望加快构建路径映射的速度(使用 tsconfig.json 中的 paths 选项)。对于包含数百个映射的项目,由此带来的性能提升相当显著。
使用 –strict 加快增量构建
我们发现了一个 bug,即如果 --strict 处于启用状态,那么 TypeScript 最终会在 --incremental 编译下重新执行类型检查。这会导致不少构建操作如同 --incremental 被关闭了一样缓慢。TypeScript 4.4 修复了这个问题,同时也将修复成果向下移植到了 TypeScript 4.3 当中。
为大型输出更快生成源映射
TypeScript 4.4 为超大输出文件提供了源映射生成优化功能。与旧版 TypeScript 编译器相比,新版本的发布时长可缩短约 8%。
--force 构建速度更快
在项目引用中使用 --build 模式时,TypeScript 必须执行最新检查以确定需要重建哪些文件。但在执行 --force 构建时,TypeScript 却不会使用这部分信息,而是对所有项目依赖项均从零开始构建。在 TypeScript 4.4 中,--force 构建也能根据检查结果确定需要重建的具体文件了。
TypeScript 为 Visual Studio 及 Visual Studio Code 等编辑器中的 JavaScript 编辑体验提供支持。大多数情况下,TypeScript 会尽量不干涉 JavaScript 文件,但也会根据实际情况提出一些置信度高、且不太具有破坏性影响的建议方法。
因此,现在即使是没有开启 // @ts-check 或者 checkJs 的项目,TypeScript 也会为纯 JavaScript 文件提供拼写建议。这些建议与 TypeScript 文件中的“Did you mean…?”形式完全相同。
拼写建议中的线索能够帮助您查找代码中的错误。我们也在测试中成功从现有代码中找出了不少错误!
关于此项功能的更多详细信息,请 参阅 pull 请求。
TypeScript 4.4 提供对 inlay hints 的支持,可帮助您在代码中显示有用信息,包括参数名称与返回类型。这相当于一种友好的“幽灵文本”。
这项功能由 Wenlu Wang 贡献,点击下方链接可查看 pull 请求中的详细信息。
https://github.com/microsoft/TypeScript/pull/42089
Wenlu 还贡献了 Visual Studio Code 中的 inlay hints 集成,并随 2021 年 7 月的 1.59 版本共同发布。如果您想体验 inlay hints,请确保您使用的是最新的稳定版或内部版编辑器。您也可以在修改设置中调整 inlay hints 提示的时间与位置。
在 Visual Studio Code 等编辑器显示完成列表时,具有自动导入的完成结果会在显示中包含对于特定模块的路径。然而,此路径往往并不是由 TypeScript 亲自放置在模块说明当中。此路径通常与工作区相关,所以如果您是从 moment 等工具包处进行导入,则会看到 node_modules/moment 之类的路径。
因为没有正确考虑到 Node 的 node_modules 解析、路径映射、符号链接与重新导出等因素,这些路径往往会产生一定的误导效果。
由于这项功能会带来较高的计算资源需求,因此在键入大量字符时,包含众多自动导入的完成项列表可能会批量填充最终模块说明。所以有时候您看到的可能仍是旧的工作区相关路径标签;但随着编辑器的不断“预热”,您应该很快就会看到正确的导入路径。
TypeScript 4.4 中的 lib.d.ts 变更
与之前的各个版本一样,TypeScript 4.4 中的 lib.d.ts 声明(特别是为 Web 上下文生成的声明)再次变更。您可以参阅我们的 lib.dom.d.ts 变更列表以了解新增内容。
间接调用导入函数以提升合规性
在其他早期版本中,从 CommonJS、AMD 以及其他非 ES 模块系统处执行的导入调用操作会设置所调用函数的 this 值。具体来讲,在以下示例中,当我们调用 fooModule.foo() 时, foo() 方法会将 fooModule 设置为 this 的值。
// 假设这是我们导入的模块,它有一个名为'foo'的导出。
let fooModule = {
foo() {
console.log(this);
}
};
fooModule.foo();
但 ECMAScript 在处理导出函数时的方式与此不同。所以,我们才决定在 TypeScript 4.4 的导入函数调用中丢弃掉 this 值。
// 假设这是我们导入的模块,它有一个名为'foo'的导出。
let fooModule = {
foo() {
console.log(this);
}
};
// 请注意,现在我们实际调用的是'(0, fooModule.foo)' 。
(0, fooModule.foo)();
在 Catch 变量中使用 unknown
用户在运行 --strict 标记时可能看到关于 catch 变量为 unknown 的新错误,特别是在现有代码假定只捕捉了 Error 值的时候。这通常会引发发下错误提示:
-
类型'unknown'上不存在属性'message'。
-
类型'unknown'上不存在属性'name'。
-
类型'unknown'上不存在属性'stack'。
要解决这个问题,您可以添加专门的运行时检查以保证抛出的类型与您的预期类型相符。此外,您也可以使用类型断言,向您的 catch 变量添加显式的: any,或者干脆关闭 --useUnknownInCatchVariables。
更广泛的始终为真承诺检查
在之前的版本中,TypeScript 引用了“始终为真承诺检查”(Always Truthy Promise checks)来捕捉可能遗留有 await 的代码。但这项检查仅适用于命名声明,所以虽然代码能够正确接收到错误,但:
async function foo(): Promise<boolean> {
return false;
}
async function bar(): Promise<string> {
const fooResult = foo();
if (fooResult) { // <- 出错了! :D
return "true";
}
return "false";
}
……以下代码却得不到提示。
async function foo(): Promise<boolean> {
return false;
}
async function bar(): Promise<string> {
if (foo()) { // <- 没有错误提示 :(
return "true";
}
return "false";
}
TypeScript 4.4 现在能够对二者做出正确标记。
抽象属性不能有初始化器
以下代码现在会引发错误,这是因为抽象属性不能有初始化器:
abstract class C {
abstract prop = 1;
// ~~~~
// 因为被标记为抽象,所以属性'prop' 不能有初始化器。
}
相反,您只能为属性指定类型:
abstract class C {
abstract prop: number;
}
我们计划在未来几周之内发布 TypeScript 4.4 的稳定版,感兴趣的朋友请持续关注我们的 4.4 迭代计划。具体链接:
https://github.com/microsoft/TypeScript/issues/44237
原文链接:https://devblogs.microsoft.com/typescript/announcing-typescript-4-4-rc/
明天一早,我会连麦知名技术专家左耳朵耗子。和他聊聊前端,聊聊创业,聊聊学习和成长,如果明天你没事,可以一起来学习下。
文章评论