Skip to content
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待

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(); // 哎呀
  //  ~~~~
  // 对象可能为 'undefined'。
}

这是一种奇怪的行为! sealExhibit 包含 undefined 以某种方式污染了 lion 的类型,使其包含 undefined

得益于 Jack Bates一个拉取请求,TypeScript 3.9 中推断过程的改进已经修复了此问题。 上述代码不再报错。 如果你因为 Promise 相关问题而停留在旧版本的 TypeScript 上,我们鼓励你尝试一下 3.9!

awaited 类型呢?

如果你一直在关注我们的议题跟踪器和设计会议记录,你可能已经了解了一些关于一个名为 awaited 的新类型操作符的工作。 这个类型操作符的目标是精确地模拟 JavaScript 中 Promise 展开的工作方式。

我们最初计划在 TypeScript 3.9 中发布 awaited,但当我们用现有代码库运行早期的 TypeScript 构建时,我们意识到该功能需要更多的设计工作,然后才能顺利地向所有人推出。 因此,我们决定将该功能从我们的主分支中移除,直到我们更有信心。 我们将对该功能进行更多实验,但不会将其作为此版本的一部分发布。

速度改进

TypeScript 3.9 带来了许多新的速度改进。 在观察到像 material-ui 和 styled-components 等包的编辑/编译速度极慢之后,我们的团队一直在关注性能。 我们深入研究了这个问题,进行了一系列不同的拉取请求,优化了涉及大型联合、交集、条件类型和映射类型的某些病态情况。

这些拉取请求中的每一个都在某些代码库上获得了约 5-10% 的编译时间减少。 总的来说,我们相信我们已经将 material-ui 的编译时间减少了约 40%!

我们还在编辑器场景中对文件重命名功能进行了一些更改。 我们从 Visual Studio Code 团队那里听说,在重命名文件时,仅确定需要更新的导入语句就可能需要 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");

  // 做一些事情
}

因此,当 TypeScript 用户误用此函数时,他们会收到有用的红色波浪线和错误消息,而 JavaScript 用户会收到断言错误。 我们想要测试此行为,因此我们将编写一个单元测试。

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

不幸的是,如果我们的测试是用 TypeScript 编写的,TypeScript 会给我们一个错误!

ts
doStuff(123, 456);
//          ~~~
// 错误:类型 'number' 不能赋值给类型 '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 拉取请求

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 {
  // ...
}

// 哎呀!
if (hasImportantPermissions) {
  //  ~~~~~~~~~~~~~~~~~~~~~~~
  // 此条件将始终返回 true,因为函数总是已定义。
  // 你是想调用它吗?
  deleteAllTheImportantFiles();
}

然而,此错误仅适用于 if 语句中的条件。 得益于 Alexander Tarasyuk一个拉取请求,此功能现在也支持三元条件(即 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
      ? //     ~~~~~~~~~~~
        // 此条件将始终返回 true
        // 因为函数总是已定义。
        // 你是想调用它吗?
        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 现在会自动检测你正在使用的导入类型,以保持你的文件风格清晰一致。

有关此更改的更多详细信息,请参阅相应的拉取请求

代码操作保留换行符

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

在旧版本的 TypeScript 中将 for 循环提取到函数中。换行符没有被保留。

这不理想——我们在 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);
  }
}

在 TypeScript 3.9 中将 for 循环提取到函数中。换行符被保留了。

你可以在此拉取请求中查看更多关于实现的信息

缺少返回表达式的快速修复

有时我们可能会忘记返回函数中最后一个语句的值,尤其是在为箭头函数添加花括号时。

ts
// 之前
let f1 = () => 42;

// 哎呀 - 不一样了!
let f2 = () => {
  42;
};

感谢社区成员 Wenlu Wang一个拉取请求,TypeScript 可以提供一个快速修复来添加缺失的 return 语句、删除花括号,或者为看起来可疑地像对象字面量的箭头函数体添加括号。

TypeScript 通过添加 return 语句或删除花括号来修复没有返回表达式的错误。

支持“解决方案风格”的 tsconfig.json 文件

编辑器需要找出一个文件属于哪个配置文件,以便它可以应用适当的选项并找出当前“项目”中包含哪些其他文件。 默认情况下,由 TypeScript 语言服务器提供支持的编辑器通过向上遍历每个父目录来查找 tsconfig.json 来实现这一点。

这种情况稍微有点问题的是,当 tsconfig.json 仅仅是为了引用其他 tsconfig.json 文件而存在时。

jsonc
// tsconfig.json
{
  "files": [],
  "references": [
    { "path": "./tsconfig.shared.json" },
    { "path": "./tsconfig.frontend.json" },
    { "path": "./tsconfig.backend.json" }
  ]
}

这个文件除了管理其他项目文件之外什么也不做,在某些环境中通常被称为“解决方案”。 在这里,这些 tsconfig.*.json 文件都没有被服务器识别,但我们确实希望语言服务器理解当前的 .ts 文件可能属于这个根 tsconfig.json 中提到的项目之一。

TypeScript 3.9 为这种配置增加了对编辑场景的支持。 有关更多详细信息,请查看添加此功能的拉取请求

破坏性更改

可选链和非空断言中的解析差异

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 强制执行的拉取请求,你会收到类似这样的错误消息

Unexpected token. Did you mean `{'>'}` or `&gt;`?
Unexpected token. Did you mean `{'}'}` or `&rbrace;`?

例如:

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

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

对交集和可选属性的更严格检查

通常,如果 AB 可赋值给 C,那么像 A & B 这样的交集类型就可赋值给 C;然而,有时在可选属性上会出现问题。 例如,考虑以下情况:

ts
interface A {
  a: number; // 注意这是 'number'
}

interface B {
  b: string;
}

interface C {
  a?: boolean; // 注意这是 'boolean'
  b: string;
}

declare let x: A & B;
declare let y: C;

y = x;

在以前的 TypeScript 版本中,这是允许的,因为虽然 AC 完全不兼容,但 B C 兼容。

在 TypeScript 3.9 中,只要交集类型中的每个类型都是具体的对象类型,类型系统就会同时考虑所有属性。 因此,TypeScript 将看到 A & Ba 属性与 Ca 属性不兼容:

Type 'A & B' is not assignable to type 'C'.
  Types of property 'a' are incompatible.
    Type 'number' is not assignable to type 'boolean | undefined'.

有关此更改的更多信息,请参阅相应的拉取请求

由可辨识属性缩减的交集

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

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'.

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

Getter/Setter 不再是可枚举的

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

感谢 GitHub 用户 pathurs一个拉取请求,TypeScript 3.9 在这方面更符合 ECMAScript。

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

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

ts
function foo<T extends any>(arg: T) {
  arg.spfjgerijghoied; // 没有错误!
}

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

ts
function foo<T extends any>(arg: T) {
  arg.spfjgerijghoied;
  //  ~~~~~~~~~~~~~~~
  // 属性 'spfjgerijghoied' 在类型 '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;
}