TypeScript 3.9

类型推断与 Promise.all 的改进

近期版本的 TypeScript(大约 3.7 左右)更新了 Promise.allPromise.race 等函数的声明。遗憾的是,这引入了一些回归问题,尤其是在混合使用包含 nullundefined 的值时。

ts
interface 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 BatesPull 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,优化了涉及大型联合类型、交叉类型、条件类型和映射类型的某些特殊情况。

这些 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 用户提供有用的错误提示。

ts
function doStuff(abc: string, xyz: string) {
assert(typeof abc === "string");
assert(typeof xyz === "string");
// do some stuff
}

这样,TypeScript 用户在误用该函数时会得到红色的波浪线和错误消息,而 JavaScript 用户会得到一个断言错误。我们希望测试这种行为,所以我们编写了一个单元测试。

ts
expect(() => {
doStuff(123, 456);
}).toThrow();

遗憾的是,如果我们的测试是用 TypeScript 编写的,TypeScript 会给我们报错!

ts
doStuff(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-error
console.log(47 * "octopus");

而以下代码

ts
// @ts-expect-error
console.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 中,我们引入了未调用函数检查,用于在您忘记调用函数时报告错误。

ts
function 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 TarasyukPull Request,此功能现在也支持三元条件表达式(即 cond ? trueExpr : falseExpr 语法)。

ts
declare 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 功能的方式因编辑器而异,但:

JavaScript 中的 CommonJS 自动导入

一个重大的改进在于使用 CommonJS 模块的 JavaScript 文件中的自动导入。

在旧版本中,TypeScript 总是假设无论您的文件是什么,您都想要 ECMAScript 风格的导入,例如

js
import * as fs from "fs";

然而,并非每个人在编写 JavaScript 文件时都以 ECMAScript 风格的模块为目标。许多用户仍在使用 CommonJS 风格的 require(...) 导入,如下所示

js
const fs = require("fs");

TypeScript 现在会自动检测您正在使用的导入类型,以保持文件风格的整洁和一致。

有关该更改的更多详细信息,请参见 相应的 Pull Request

代码操作保留换行符

TypeScript 的重构和快速修复在保留换行符方面通常做得不够好。作为一个非常基础的例子,请看以下代码。

ts
const 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*/ 的范围并提取到新函数中,我们将得到如下代码。

ts
const 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);
}
}

Extracting the for loop to a function in older versions of TypeScript. A newline is not preserved.

这并不理想——我们的 for 循环中每条语句之间有一个空行,但重构将其去掉了!TypeScript 3.9 做了更多工作来保留我们所写的代码格式。

ts
const 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);
}
}

Extracting the for loop to a function in TypeScript 3.9. A newline is preserved.

您可以在 此 Pull Request 中查看有关实现的更多信息

缺少 return 表达式的快速修复

有时我们可能会忘记返回函数中最后一条语句的值,特别是在向箭头函数添加大括号时。

ts
// before
let f1 = () => 42;
// oops - not the same!
let f2 = () => {
42;
};

多亏了来自社区成员 Wenlu WangPull Request,TypeScript 可以提供快速修复来添加丢失的 return 语句、删除大括号,或为看起来像对象字面量的箭头函数体添加括号。

TypeScript fixing an error where no expression is returned by adding a return statement or removing curly braces.

支持“解决方案风格”(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 最近实现了可选链运算符,但我们收到用户反馈称,可选链 (?.) 与非空断言运算符 (!) 的行为极其违反直觉。

具体来说,在之前的版本中,代码

ts
foo?.bar!.baz;

被解释为等同于以下 JavaScript。

js
(foo?.bar).baz;

在上述代码中,括号停止了可选链的“短路”行为,因此如果 fooundefined,访问 baz 将导致运行时错误。

指出此行为的 Babel 团队以及大多数向我们提供反馈的用户都认为这种行为是错误的。我们也这么认为!我们听到的最多的是 ! 运算符应该直接“消失”,因为我们的意图是从 bar 的类型中移除 nullundefined

换句话说,大多数人认为原始代码片段应该被解释为

js
foo?.bar.baz;

fooundefined 时,它只会求值为 undefined

这是一个重大更改,但我们相信大多数代码在编写时都是考虑到这种新解释的。希望恢复旧行为的用户可以在 ! 运算符的左侧添加明确的括号。

ts
foo?.bar!.baz;

}> 现在是无效的 JSX 文本字符

JSX 规范禁止在文本位置使用 }> 字符。TypeScript 和 Babel 都决定执行此规则以更符合规范。插入这些字符的新方法是使用 HTML 转义代码(例如 <span> 2 &gt 1 </span>)或插入带有字符串字面量的表达式(例如 <span> 2 {">"} 1 </span>)。

幸运的是,多亏了来自 Brad Zacher 执行此规则的 Pull Request,您将收到如下类似的错误消息

Unexpected token. Did you mean `{'>'}` or `>`?
Unexpected token. Did you mean `{'}'}` or `}`?

例如:

tsx
let directions = <span>Navigate to: Menu Bar > Tools > Options</span>;
// ~ ~
// Unexpected token. Did you mean `{'>'}` or `>`?

该错误消息带有一个便捷的快速修复,并且多亏了 Alexander Tarasyuk,如果您有很多错误,您可以批量应用这些更改

对交叉类型和可选属性的更严格检查

通常,如果 AB 可分配给 C,则交叉类型 A & B 可分配给 C;然而,这有时在可选属性上会有问题。例如,请看以下代码

ts
interface 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 版本中,这是被允许的,因为虽然 AC 完全不兼容,但 B 确实C 兼容。

在 TypeScript 3.9 中,只要交叉类型中的每个类型都是具体的对象类型,类型系统就会同时考虑所有属性。因此,TypeScript 会发现 A & Ba 属性与 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

通过判别属性缩减交叉类型

在某些情况下,您可能会得到描述根本不存在的值的类型。例如

ts
declare 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);

这段代码有点奇怪,因为确实没有办法创建 CircleSquare 的交集——它们有两个不兼容的 kind 字段。在之前的 TypeScript 版本中,此代码被允许,并且 kind 本身的类型为 never,因为 "circle" & "square" 描述了一组永不存在的值。

在 TypeScript 3.9 中,类型系统在此处更加激进——它注意到由于 kind 属性的存在,无法交叉 CircleSquare。因此,它没有将 z.kind 的类型折叠为 never,而是将 z 本身 (Circle & Square) 的类型折叠为 never。这意味着上面的代码现在会报错

Property 'kind' does not exist on type 'never'.

我们观察到的大多数破坏似乎都对应于稍微不正确的类型声明。有关更多详细信息,请参阅原始 Pull Request

Getter/Setter 不再可枚举

在旧版本的 TypeScript 中,类中的 getset 访问器以一种使它们可枚举的方式发出;然而,这不符合 ECMAScript 规范,该规范规定它们必须是不可枚举的。因此,目标为 ES5 和 ES2015 的 TypeScript 代码在行为上可能会有所不同。

多亏了来自 GitHub 用户 pathursPull Request,TypeScript 3.9 现在在这方面更符合 ECMAScript。

扩展 any 的类型参数不再表现为 any

在之前的 TypeScript 版本中,约束为 any 的类型参数可以被视为 any

ts
function foo<T extends any>(arg: T) {
arg.spfjgerijghoied; // no error!
}

这是一个疏忽,因此 TypeScript 3.9 采取了更保守的方法,并对这些可疑操作发出错误。

ts
function 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 中即可恢复它们

ts
interface AudioTrackList {
[Symbol.iterator](): IterableIterator<AudioTrack>;
}
interface HTMLVideoElement {
readonly audioTracks: AudioTrackList
msFrameStep(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;
}

TypeScript 文档是一个开源项目。请发送 Pull Request 帮助我们改进这些页面 ❤

此页面的贡献者
OTOrta Therox (12)
SLShammel Lee (1)
NSNick Schonning (1)
MUMasato Urai (1)
HKHomyee King (1)
2+

最后更新:2026 年 3 月 27 日