类型推断与 Promise.all 的改进
近期版本的 TypeScript(大约 3.7 左右)更新了 Promise.all 和 Promise.race 等函数的声明。遗憾的是,这引入了一些回归问题,尤其是在混合使用包含 null 或 undefined 的值时。
tsinterface Lion {roar(): void;}interface Seal {singKissFromARose(): void;}async function visitZoo(lionExhibit: Promise<Lion>,sealExhibit: Promise<Seal | undefined>) {let [lion, seal] = await Promise.all([lionExhibit, sealExhibit]);lion.roar(); // uh oh// ~~~~// Object is possibly 'undefined'.}
这是一种奇怪的行为!sealExhibit 中包含一个 undefined,导致 lion 的类型被“污染”并包含了 undefined。
多亏了来自 Jack Bates 的 Pull Request,这一问题已在 TypeScript 3.9 中通过改进推断流程得到修复。上述代码不再报错。如果您因 Promise 相关问题而停留在旧版 TypeScript,我们鼓励您尝试一下 3.9!
关于 awaited 类型?
如果您一直在关注我们的问题跟踪器和设计会议记录,可能已经了解到关于 名为 awaited 的新类型运算符 的工作。该类型运算符的目标是准确模拟 JavaScript 中 Promise 的解包(unwrapping)方式。
我们最初预计在 TypeScript 3.9 中发布 awaited,但在使用现有代码库运行早期的 TypeScript 构建版本后,我们意识到该功能在顺利推广给所有人之前还需要更多的设计工作。因此,我们决定将该功能从主分支中撤出,直到我们更有把握为止。我们将继续对该功能进行试验,但不会将其作为本次发布的一部分。
速度改进
TypeScript 3.9 带来了多项速度改进。在观察到使用 material-ui 和 styled-components 等包时出现极其缓慢的编辑/编译速度后,我们的团队一直专注于性能优化。我们进行了深入研究,通过一系列不同的 Pull Request,优化了涉及大型联合类型、交叉类型、条件类型和映射类型的某些特殊情况。
- https://github.com/microsoft/TypeScript/pull/36576
- https://github.com/microsoft/TypeScript/pull/36590
- https://github.com/microsoft/TypeScript/pull/36607
- https://github.com/microsoft/TypeScript/pull/36622
- https://github.com/microsoft/TypeScript/pull/36754
- https://github.com/microsoft/TypeScript/pull/36696
这些 Pull Request 中的每一项都使特定代码库的编译时间减少了约 5-10%。总的来说,我们认为 material-ui 的编译时间减少了约 40%!
我们还对编辑器场景中的文件重命名功能进行了一些更改。我们从 Visual Studio Code 团队获悉,在重命名文件时,仅确定需要更新哪些 import 语句可能就需要 5 到 10 秒。TypeScript 3.9 通过 更改编译器和语言服务缓存文件查找的内部机制 解决了这个问题。
虽然仍有改进空间,但我们希望这项工作能为每个人带来更迅捷的体验!
// @ts-expect-error 注释
想象一下,我们正在用 TypeScript 编写一个库,并导出了一个名为 doStuff 的函数作为公共 API 的一部分。该函数的类型声明它接受两个 string 参数,以便其他 TypeScript 用户可以获得类型检查错误,但它也会执行运行时检查(可能仅在开发版本中),以便给 JavaScript 用户提供有用的错误提示。
tsfunction doStuff(abc: string, xyz: string) {assert(typeof abc === "string");assert(typeof xyz === "string");// do some stuff}
这样,TypeScript 用户在误用该函数时会得到红色的波浪线和错误消息,而 JavaScript 用户会得到一个断言错误。我们希望测试这种行为,所以我们编写了一个单元测试。
tsexpect(() => {doStuff(123, 456);}).toThrow();
遗憾的是,如果我们的测试是用 TypeScript 编写的,TypeScript 会给我们报错!
tsdoStuff(123, 456);// ~~~// error: Type 'number' is not assignable to type 'string'.
这就是为什么 TypeScript 3.9 带来了新功能:// @ts-expect-error 注释。当一行代码前面有 // @ts-expect-error 注释时,TypeScript 将抑制该错误的报告;但如果那里没有错误,TypeScript 将报错指出 // @ts-expect-error 是不必要的。
作为简单示例,以下代码是正常的
ts// @ts-expect-errorconsole.log(47 * "octopus");
而以下代码
ts// @ts-expect-errorconsole.log(1 + 1);
会导致错误
Unused '@ts-expect-error' directive.
我们要衷心感谢实现此功能的贡献者 Josh Goldberg。更多信息请查看 ts-expect-error 的 Pull Request。
ts-ignore 还是 ts-expect-error?
在某些方面,// @ts-expect-error 可以作为抑制注释,类似于 // @ts-ignore。不同之处在于,如果下一行代码没有错误,// @ts-ignore 不会执行任何操作。
您可能想将现有的 // @ts-ignore 注释切换为 // @ts-expect-error,也可能在考虑未来代码中该选哪一个。虽然这完全取决于您和您的团队,但我们对在特定情况下如何选择有一些建议。
在以下情况下选择 ts-expect-error:
- 您正在编写测试代码,并且实际上希望类型系统在操作时报错
- 您预期很快会有修复,并且只需要一个快速的临时方案
- 您处于一个规模适中、团队积极向上的项目中,希望在受影响的代码再次合法后尽快移除抑制注释
在以下情况下选择 ts-ignore:
- 您有一个较大的项目,并且在没有明确所有者的代码中出现了新错误
- 您正在两个不同版本的 TypeScript 之间进行升级,一行代码在一个版本中报错但在另一个版本中不报错
- 您实在没时间决定这两个选项中哪个更好
条件表达式中的未调用函数检查
在 TypeScript 3.7 中,我们引入了未调用函数检查,用于在您忘记调用函数时报告错误。
tsfunction hasImportantPermissions(): boolean {// ...}// Oops!if (hasImportantPermissions) {// ~~~~~~~~~~~~~~~~~~~~~~~// This condition will always return true since the function is always defined.// Did you mean to call it instead?deleteAllTheImportantFiles();}
然而,该错误仅适用于 if 语句中的条件。多亏了来自 Alexander Tarasyuk 的 Pull Request,此功能现在也支持三元条件表达式(即 cond ? trueExpr : falseExpr 语法)。
tsdeclare function listFilesOfDirectory(dirPath: string): string[];declare function isDirectory(): boolean;function getAllFiles(startFileName: string) {const result: string[] = [];traverse(startFileName);return result;function traverse(currentPath: string) {return isDirectory? // ~~~~~~~~~~~// This condition will always return true// since the function is always defined.// Did you mean to call it instead?listFilesOfDirectory(currentPath).forEach(traverse): result.push(currentPath);}}
https://github.com/microsoft/TypeScript/issues/36048
编辑器改进
TypeScript 编译器不仅为大多数主要编辑器提供 TypeScript 编辑体验,还为 Visual Studio 系列编辑器等提供 JavaScript 体验。在编辑器中使用新的 TypeScript/JavaScript 功能的方式因编辑器而异,但:
- Visual Studio Code 支持 选择不同版本的 TypeScript。此外,还有 JavaScript/TypeScript Nightly 扩展 来保持前沿体验(通常非常稳定)。
- Visual Studio 2017/2019 拥有 [上述 SDK 安装程序] 和 MSBuild 安装。
- Sublime Text 3 支持 选择不同版本的 TypeScript
JavaScript 中的 CommonJS 自动导入
一个重大的改进在于使用 CommonJS 模块的 JavaScript 文件中的自动导入。
在旧版本中,TypeScript 总是假设无论您的文件是什么,您都想要 ECMAScript 风格的导入,例如
jsimport * as fs from "fs";
然而,并非每个人在编写 JavaScript 文件时都以 ECMAScript 风格的模块为目标。许多用户仍在使用 CommonJS 风格的 require(...) 导入,如下所示
jsconst fs = require("fs");
TypeScript 现在会自动检测您正在使用的导入类型,以保持文件风格的整洁和一致。
有关该更改的更多详细信息,请参见 相应的 Pull Request。
代码操作保留换行符
TypeScript 的重构和快速修复在保留换行符方面通常做得不够好。作为一个非常基础的例子,请看以下代码。
tsconst maxValue = 100;/*start*/for (let i = 0; i <= maxValue; i++) {// First get the squared value.let square = i ** 2;// Now print the squared value.console.log(square);}/*end*/
如果我们突出显示编辑器中从 /*start*/ 到 /*end*/ 的范围并提取到新函数中,我们将得到如下代码。
tsconst maxValue = 100;printSquares();function printSquares() {for (let i = 0; i <= maxValue; i++) {// First get the squared value.let square = i ** 2;// Now print the squared value.console.log(square);}}

这并不理想——我们的 for 循环中每条语句之间有一个空行,但重构将其去掉了!TypeScript 3.9 做了更多工作来保留我们所写的代码格式。
tsconst maxValue = 100;printSquares();function printSquares() {for (let i = 0; i <= maxValue; i++) {// First get the squared value.let square = i ** 2;// Now print the squared value.console.log(square);}}

您可以在 此 Pull Request 中查看有关实现的更多信息
缺少 return 表达式的快速修复
有时我们可能会忘记返回函数中最后一条语句的值,特别是在向箭头函数添加大括号时。
ts// beforelet f1 = () => 42;// oops - not the same!let f2 = () => {42;};
多亏了来自社区成员 Wenlu Wang 的 Pull Request,TypeScript 可以提供快速修复来添加丢失的 return 语句、删除大括号,或为看起来像对象字面量的箭头函数体添加括号。

支持“解决方案风格”(Solution Style)的 tsconfig.json 文件
编辑器需要确定一个文件属于哪个配置文件,以便应用适当的选项并找出当前“项目”中还包含哪些文件。默认情况下,由 TypeScript 语言服务器驱动的编辑器通过向上遍历每个父目录来查找 tsconfig.json 来做到这一点。
这种情况在 tsconfig.json 仅用于引用其他 tsconfig.json 文件时会略微失效。
// tsconfig.json{"": [],"": [{ "path": "./tsconfig.shared.json" },{ "path": "./tsconfig.frontend.json" },{ "path": "./tsconfig.backend.json" }]}
这种只负责管理其他项目文件的文件在某些环境中通常被称为“解决方案”(solution)。在这里,这些 tsconfig.*.json 文件都不会被服务器拾取,但我们确实希望语言服务器能理解当前的 .ts 文件很可能属于此根 tsconfig.json 中提到的项目之一。
TypeScript 3.9 为编辑场景增加了对该配置的支持。有关更多详细信息,请查看 添加此功能的 Pull Request。
破坏性变更
可选链和非空断言解析的差异
TypeScript 最近实现了可选链运算符,但我们收到用户反馈称,可选链 (?.) 与非空断言运算符 (!) 的行为极其违反直觉。
具体来说,在之前的版本中,代码
tsfoo?.bar!.baz;
被解释为等同于以下 JavaScript。
js(foo?.bar).baz;
在上述代码中,括号停止了可选链的“短路”行为,因此如果 foo 是 undefined,访问 baz 将导致运行时错误。
指出此行为的 Babel 团队以及大多数向我们提供反馈的用户都认为这种行为是错误的。我们也这么认为!我们听到的最多的是 ! 运算符应该直接“消失”,因为我们的意图是从 bar 的类型中移除 null 和 undefined。
换句话说,大多数人认为原始代码片段应该被解释为
jsfoo?.bar.baz;
当 foo 为 undefined 时,它只会求值为 undefined。
这是一个重大更改,但我们相信大多数代码在编写时都是考虑到这种新解释的。希望恢复旧行为的用户可以在 ! 运算符的左侧添加明确的括号。
tsfoo?.bar!.baz;
} 和 > 现在是无效的 JSX 文本字符
JSX 规范禁止在文本位置使用 } 和 > 字符。TypeScript 和 Babel 都决定执行此规则以更符合规范。插入这些字符的新方法是使用 HTML 转义代码(例如 <span> 2 > 1 </span>)或插入带有字符串字面量的表达式(例如 <span> 2 {">"} 1 </span>)。
幸运的是,多亏了来自 Brad Zacher 执行此规则的 Pull Request,您将收到如下类似的错误消息
Unexpected token. Did you mean `{'>'}` or `>`?Unexpected token. Did you mean `{'}'}` or `}`?
例如:
tsxlet directions = <span>Navigate to: Menu Bar > Tools > Options</span>;// ~ ~// Unexpected token. Did you mean `{'>'}` or `>`?
该错误消息带有一个便捷的快速修复,并且多亏了 Alexander Tarasyuk,如果您有很多错误,您可以批量应用这些更改。
对交叉类型和可选属性的更严格检查
通常,如果 A 或 B 可分配给 C,则交叉类型 A & B 可分配给 C;然而,这有时在可选属性上会有问题。例如,请看以下代码
tsinterface A {a: number; // notice this is 'number'}interface B {b: string;}interface C {a?: boolean; // notice this is 'boolean'b: string;}declare let x: A & B;declare let y: C;y = x;
在之前的 TypeScript 版本中,这是被允许的,因为虽然 A 与 C 完全不兼容,但 B 确实与 C 兼容。
在 TypeScript 3.9 中,只要交叉类型中的每个类型都是具体的对象类型,类型系统就会同时考虑所有属性。因此,TypeScript 会发现 A & B 的 a 属性与 C 的属性不兼容
Type 'A & B' is not assignable to type 'C'.Types of property 'a' are incompatible.Type 'number' is not assignable to type 'boolean | undefined'.
有关此更改的更多信息,请参阅相应的 Pull Request。
通过判别属性缩减交叉类型
在某些情况下,您可能会得到描述根本不存在的值的类型。例如
tsdeclare function smushObjects<T, U>(x: T, y: U): T & U;interface Circle {kind: "circle";radius: number;}interface Square {kind: "square";sideLength: number;}declare let x: Circle;declare let y: Square;let z = smushObjects(x, y);console.log(z.kind);
这段代码有点奇怪,因为确实没有办法创建 Circle 和 Square 的交集——它们有两个不兼容的 kind 字段。在之前的 TypeScript 版本中,此代码被允许,并且 kind 本身的类型为 never,因为 "circle" & "square" 描述了一组永不存在的值。
在 TypeScript 3.9 中,类型系统在此处更加激进——它注意到由于 kind 属性的存在,无法交叉 Circle 和 Square。因此,它没有将 z.kind 的类型折叠为 never,而是将 z 本身 (Circle & Square) 的类型折叠为 never。这意味着上面的代码现在会报错
Property 'kind' does not exist on type 'never'.
我们观察到的大多数破坏似乎都对应于稍微不正确的类型声明。有关更多详细信息,请参阅原始 Pull Request。
Getter/Setter 不再可枚举
在旧版本的 TypeScript 中,类中的 get 和 set 访问器以一种使它们可枚举的方式发出;然而,这不符合 ECMAScript 规范,该规范规定它们必须是不可枚举的。因此,目标为 ES5 和 ES2015 的 TypeScript 代码在行为上可能会有所不同。
多亏了来自 GitHub 用户 pathurs 的 Pull Request,TypeScript 3.9 现在在这方面更符合 ECMAScript。
扩展 any 的类型参数不再表现为 any
在之前的 TypeScript 版本中,约束为 any 的类型参数可以被视为 any。
tsfunction foo<T extends any>(arg: T) {arg.spfjgerijghoied; // no error!}
这是一个疏忽,因此 TypeScript 3.9 采取了更保守的方法,并对这些可疑操作发出错误。
tsfunction foo<T extends any>(arg: T) {arg.spfjgerijghoied;// ~~~~~~~~~~~~~~~// Property 'spfjgerijghoied' does not exist on type 'T'.}
export * 总是被保留
在以前的 TypeScript 版本中,如果 foo 没有导出任何值,则像 export * from "foo" 这样的声明会在我们的 JavaScript 输出中被删除。这种发出方式是有问题的,因为它是类型驱动的,无法由 Babel 模拟。TypeScript 3.9 将始终发出这些 export * 声明。在实践中,我们预计这不会破坏太多现有代码。
更多 libdom.d.ts 优化
我们正在继续将更多 TypeScript 的内置 .d.ts 库 (lib.d.ts 及相关系列) 移至从 DOM 规范直接生成的 Web IDL 文件。因此,删除了一些与媒体访问相关的特定厂商类型。
将此文件添加到您项目的环境 *.d.ts 中即可恢复它们
tsinterface AudioTrackList {[Symbol.iterator](): IterableIterator<AudioTrack>;}interface HTMLVideoElement {readonly audioTracks: AudioTrackListmsFrameStep(forward: boolean): void;msInsertVideoEffect(activatableClassId: string, effectRequired: boolean, config?: any): void;msSetVideoRectangle(left: number, top: number, right: number, bottom: number): void;webkitEnterFullScreen(): void;webkitEnterFullscreen(): void;webkitExitFullScreen(): void;webkitExitFullscreen(): void;msHorizontalMirror: boolean;readonly msIsLayoutOptimalForPlayback: boolean;readonly msIsStereo3D: boolean;msStereo3DPackingMode: string;msStereo3DRenderMode: string;msZoom: boolean;onMSVideoFormatChanged: ((this: HTMLVideoElement, ev: Event) => any) | null;onMSVideoFrameStepCompleted: ((this: HTMLVideoElement, ev: Event) => any) | null;onMSVideoOptimalLayoutChanged: ((this: HTMLVideoElement, ev: Event) => any) | null;webkitDisplayingFullscreen: boolean;webkitSupportsFullscreen: boolean;}interface MediaError {readonly msExtendedCode: number;readonly MS_MEDIA_ERR_ENCRYPTED: number;}