[译] TypeScript 4.8 beta 更新

这里是 TypeScript 4.8 版本的更新清单:

  • 改进的交叉点减少、联合兼容性和收窄
  • 改进了类型系统中对于模板字符串的 Infer 的推断
  • --build、--watch 和 --incremental 的性能提升
  • 比较数组和对象时的错误(新特性)
  • 从绑定模式改进推断
  • 修复文件监视(尤其是 git checkout)
  • Find-All-References 性能提升
  • 重大变化

一、改进的交叉点减少、联合兼容性和收窄

TypeScript 4.8 在 --strictNullChecks 下带来了一系列正确性和一致性的改进。这些更改会影响交集(&)和联合类型的工作方式,并在 TypeScript 如何缩小类型时加以利用。 例如,unknown 在精神上与联合类型 {} | null | undefined 很接近, 因为它接受 nullundefined 和任何其他类型。 TypeScript 现在可以识别这一点,并允许从未知分配给** {} | null | undefined**。

function f(x: unknown, y: {} | null | undefined) {
    x = y; // always worked
    y = x; // used to error, now works
}

另一个变化是 {} 与任何其他对象类型相交会直接简化为该对象类型。这意味着我们能够重写 NonNullable 以仅使用与 {} 的交集,因为 {} & null{} & undefined 被抛弃了。

- type NonNullable<T> = T extends null | undefined ? never : T;
+ type NonNullable<T> = T & {};

这是一个改进,因为像这样的交集类型可以减少和分配,而条件类型目前不能。所以 NonNullable<NonNullable> 现在至少简化为 NonNullable,而之前没有。

function foo<T>(x: NonNullable<T>, y: NonNullable<NonNullable<T>>) {
    x = y; // always worked
    y = x; // used to error, now works
}

这些更改还使我们能够在控制流分析和类型缩小方面进行明智的改进。例如,未知现在被缩小,就像 {} | null | undefined 在真实的分支中。

function narrowUnknownishUnion(x: {} | null | undefined) {
    if (x) {
        x;  // {}
    }
    else {
        x;  // {} | null | undefined
    }
}

function narrowUnknown(x: unknown) {
    if (x) {
        x;  // used to be 'unknown', now '{}'
    }
    else {
        x;  // unknown
    }
}

通用值也同样缩小。当检查一个值不是 nullundefined 时,TypeScript 现在只是将它与 {} 相交——这再次与说它与 NonNullable 相同。将这里的许多更改放在一起,我们现在可以在没有任何类型断言的情况下定义以下函数。

function throwIfNullable<T>(value: T): NonNullable<T> {
    if (value === undefined || value === null) {
        throw Error("Nullable value!");
    }

    // Used to fail because 'T' was not assignable to 'NonNullable<T>'.
    // Now narrows to 'T & {}' and succeeds because that's just 'NonNullable<T>'.
    return value;
}

value 现在缩小为 T & {},现在与 NonNullable 相同——因此函数的主体无需特定于 TypeScript 的语法即可工作。 就其本身而言,这些变化可能看起来很小——但它们代表了多年来报告的许多 paper cuts(指用一张纸的边缘划伤皮肤) 的修复。

有关这些改进的更多详细信息,你可以在此处阅读更多信息

二、改进了类型系统中对于模板字符串的 Infer 的推断

TypeScript 最近引入了一种添加扩展约束以推断条件类型中的类型变量的方法。

type TryGetNumberIfFirst<T> =
    T extends [infer U extends number, ...unknown[]] ? U : never;

如果这些推断类型出现在模板字符串类型中并且被限制为原始类型,TypeScript 现在将尝试解析出文字类型。

// SomeNum 在过去版本是 'number'; 当前版本是 '100'.
type SomeNum = "100" extends `${infer U extends number}` ? U : never;

// SomeBigInt 在过去版本是 'bigint'; 当前版本是 '100n'.
type SomeBigInt = "100" extends `${infer U extends bigint}` ? U : never;

// SomeBool 在过去版本是 'boolean'; 当前版本是 'true'.
type SomeBool = "true" extends `${infer U extends boolean}` ? U : never;

现在可以好的传达库在运行时要做什么,并且提供更精确的类型。 值得注意的一点是当 TypeScript 解析出这些文字类型时,它会贪婪地尝试解析尽可能多的适合原始类型的样子。然而,它随后会检查该原始类型的输出值是否与字符串内容匹配。如果它没有找到字符串可以 "匹配" ,那么它将回退到基本原始类型。

// JustNumber 在这是 `number`
// 因为 TypeScript 解析出 `"1.0"`, 但是 `String(Number("1.0"))` 是 `"1"` 与 '1.0' 不匹配.
type JustNumber = "1.0" extends `${infer T extends number}` ? T : never; 

三、--build、--watch 和 --incremental 的性能提升

TypeScript 4.8 引入了一些优化,可以加速 --watch--incremental 的场景,以及使用 --build 的项目引用构建。例如,TypeScript 现在能够避免在 --watch 模式下并且无操作更改时去花费时间更新时间戳,这使得重建速度更快,并避免与可能正在监视 TypeScript 输出的其他构建工具混淆。等等 这些改进有多大?好吧,在一个相当大的内部代码库中,我们已经看到许多简单的常见操作的时间减少了 10%-25%,在无变化的情况下减少了大约 40%。我们在 TypeScript 代码库上也看到了类似的结果。 你可以在 GitHub 上查看更改以及性能结果

四、比较数组和对象时的错误(新特性)

在许多语言中,像 == 这样的运算符对对象执行所谓的“值”相等。例如,在 Python 中,通过使用 == 检查值是否等于空列表来检查列表是否为空是有效的。

if people_at_home == []:
    print("that's where she lies, broken inside. </3")

在 JavaScript 中情况并非如此,其中对象和数组之间的 == 和 === 检查两个引用是否指向同一个实例。我们认为这充其量是 JavaScript 开发人员的 foot-gun(可以理解为开枪射击自己的脚),最坏的情况是生产代码中的错误。这就是为什么 TypeScript 现在不允许像下面这样的代码。

let peopleAtHome = [];

if (peopleAtHome === []) {
  //  ~~~~~~~~~~~~~~~~~~~
  // This condition will always return 'false' since JavaScript compares objects by reference, not value.
  console.log("that's where she lies, broken inside. </3")
}

五、从绑定模式改进推断

在某些情况下,TypeScript 会从绑定模式中选择一个类型以进行更好的推断。

declare function chooseRandomly<T>(x: T, y: T): T;

let [a, b, c] = chooseRandomly([42, true, "hi!"], [0, false, "bye!"]);
//   ^  ^  ^
//   |  |  |
//   |  |  string
//   |  |
//   |  boolean
//   |
//   number

当 chooseRandomly 需要判断 T 的类型时,它会主要看 [42, true, "hi!"] 和 [0, false, "bye!"] ;但是 TypeScript 需要弄清楚这两种类型是否应该是 Array<number | boolean | string> 或元组类型 [number, boolean, string]。为此,它将寻找现有的候选者作为提示,以查看是否有任何元组类型。当 TypeScript 看到绑定模式 [a, b, c] 时,它会创建类型 [any, any, any],并且该类型被选为 T 的低优先级候选者,[42, true, "hi!"] 和 [0, false, "bye!"] 被用作类型的提示。 你可以看到这对 chooseRandomly 有什么好处,但在其他情况下它就不够用了。例如,采取以下代码

declare function f<T>(x?: T): T;

let [x, y, z] = f();

绑定模式 [x, y, z] 暗示 f 应该产生一个 [any, any, any] 元组;但是 f 真的不应该根据绑定模式改变它的类型参数。它不能根据分配的内容突然变出一个类似数组的新值,因此绑定模式类型对生成的类型有太多影响。最重要的是,因为绑定模式类型充满了 any,所以我们只剩下 x、y 和 z 被键入为 any。 在 TypeScript 4.8 中,这些绑定模式永远不会用作类型参数的候选者。相反,它们只是在某个参数需要更具体的类型(如我们的 chooseRandomly 示例中)的情况下进行协商。如果您需要恢复到旧的行为,您始终可以提供显式类型参数。 如果您想了解更多信息,可以查看 GitHub 上的更改

六、修复文件监视(尤其是 git checkout)

我们有一个长期存在的错误,即 TypeScript 在 --watch 模式和编辑器场景中更改某些文件时非常困难。有时症状是陈旧或不准确的错误,可能会出现需要重新启动 tsc 或 VS Code。这些经常发生在 Unix 系统上,您可能在使用 vim 保存文件或在 git 中交换分支后看到这些。 这是由于对 Node.js 如何跨文件系统处理重命名事件的假设造成的。 Linux 和 macOS 使用的文件系统是 inodeNode.js 会将文件观察程序附加到 inode 而不是文件路径。因此,当 Node.js 返回观察者对象时,它可能正在观察路径或索引节点,具体取决于平台和文件系统。

七、Find-All-References 性能提升

在编辑器中运行 find-all-references 时,TypeScript 现在能够更智能地聚合引用。这将 TypeScript 在其自己的代码库中搜索广泛使用的标识符所花费的时间减少了约 20%。

八、重大变化

由于类型系统更改的性质,可以进行的更改很少且不会影响某些代码;但是,有一些更改更有可能需要调整现有代码。

lib.d.ts 更新

虽然 TypeScript 努力避免重大中断,但即使是内置库中的微小更改也可能导致问题。我们预计 DOM 和 lib.d.ts 更新不会导致重大中断,但可能会有一些小的中断。

不受约束的泛型不再可分配给 {}

在 TypeScript 4.8 中,对于启用了 strictNullChecks 的项目,当在 nullundefined 不是合法值的位置使用不受约束的类型参数时,TypeScript 现在将正确地发出错误。这将包括任何需要 {}对象或具有所有可选属性的对象类型的类型。 如下面的例子:

// Accepts any non-null non-undefined value
function bar(value: {}) {
  Object.keys(value); // This call throws on null/undefined at runtime.
}

// Unconstrained type parameter T...
function foo<T>(x: T) {
    bar(x); // Used to be allowed, now is an error in 4.8.
    //  ~
    // error: Argument of type 'T' is not assignable to parameter of type '{}'.
}

foo(undefined);

如上所示,这样的代码有一个潜在的错误——值 null 和 undefined 可以通过这些不受约束的类型参数间接传递给不应该遵守这些值的代码。 此行为也将在类型位置中可见。一个例子是:

interface Foo<T> {
  x: Bar<T>; // error
}

interface Bar<T extends {}> { }

现在可以以下几种方式来加约束:

// 1. 添加 extends 约束
function foo<T extends {}>(x: T) { }

// 2. 添加判断
function foo<T>(x: T) {
  if (x !== null && x !== undefined) {
    bar(x);
  }
 }

// 3. 使用 ! 进行断言
function foo<T>(x: T) {
  bar(x!);
}

无法在 JavaScript 文件中导入/导出类型

TypeScript 以前允许 JavaScript 文件在 importexport 语句中导入和导出使用类型声明但没有值的实体。这种行为是不正确的,因为不存在的值的命名导入和导出将在 ECMAScript 模块下导致运行时错误。当在 --checkJs 下或通过 // @ts-check 注释对 JavaScript 文件进行类型检查时,TypeScript 现在将发出错误。

// @ts-check

// Will fail at runtime because 'SomeType' is not a value.
import { someValue, SomeType } from "some-module";

/**
 * @type {SomeType}
 */
export const myValue = someValue;

/**
 * @typedef {string | number} MyType
 */

// Will fail at runtime because 'MyType' is not a value.
export { MyType as MyExportedType };

要从另一个模块引用类型,您可以直接限定导入:

import { someValue } from "some-module";
  
  /**
   * @type {import("some-module").SomeType}
   */
  export const myValue = someValue;

要导出类型,您可以在 JSDoc 中使用 / @typedef /* 注释。 @typedef 注释已经自动从其包含的模块中导出类型。

  /**
   * @typedef {string | number} MyType
   */

 /**
  * @typedef {MyType} MyExportedType
  */

绑定模式不直接有助于推理候选

如上所述,绑定模式不再改变函数调用中推理结果的类型。你可以在此处阅读有关原始更改的更多信息

九、总结

其实有时候有些事情只是想象的困难和麻烦,真正做起来可能也没多困难。

此次 beta 更新多是对于收窄类型和性能优化方面的更改,不过大家要多关注重大变化部分内容,可能需要做一些更改。

猜你喜欢

转载自juejin.im/post/7112947994680360996
4.8