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

TypeScript 4.8

改进的交集简化、联合兼容性和收窄

TypeScript 4.8 在 --strictNullChecks 下带来了一系列正确性和一致性的改进。 这些更改影响了交集和联合类型的工作方式,并在 TypeScript 收窄类型时得到了利用。

例如,unknown 在精神上接近联合类型 {} | null | undefined,因为它接受 nullundefined 以及任何其他类型。 TypeScript 现在认识到了这一点,并允许从 unknown 赋值给 {} | null | undefined

ts
function f(x: unknown, y: {} | null | undefined) {
    x = y; // 始终有效
    y = x; // 以前报错,现在有效
}

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

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

这是一个改进,因为像这样的交集类型可以被简化和赋值,而条件类型目前不能。 因此,NonNullable<NonNullable<T>> 现在至少简化为 NonNullable<T>,而以前则不能。

ts
function foo<T>(x: NonNullable<T>, y: NonNullable<NonNullable<T>>) {
    x = y; // 始终有效
    y = x; // 以前报错,现在有效
}

这些更改还使我们能够在控制流分析和类型收窄方面引入合理的改进。 例如,unknown 现在在真值分支中像 {} | null | undefined 一样被收窄。

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

function narrowUnknown(x: unknown) {
    if (x) {
        x;  // 以前是 'unknown',现在是 '{}'
    }
    else {
        x;  // unknown
    }
}

泛型值也以类似方式被收窄。 当检查一个值不是 nullundefined 时,TypeScript 现在将其与 {} 相交——同样,这与说它是 NonNullable 是一样的。 综合这里的许多更改,我们现在可以定义以下函数而无需任何类型断言。

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

    // 以前会失败,因为 'T' 不能赋值给 'NonNullable<T>'。
    // 现在收窄为 'T & {}' 并且成功,因为那正是 'NonNullable<T>'。
    return value;
}

value 现在被收窄为 T & {},并且与 NonNullable<T> 相同——因此函数体无需任何 TypeScript 特定语法即可工作。

就其本身而言,这些更改可能看起来很小——但它们代表了对多年来报告的许多细小问题的修复。

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

模板字符串类型中 infer 类型的改进推断

TypeScript 最近引入了一种在条件类型中向 infer 类型变量添加 extends 约束的方法。

ts
// 如果元组的第一个元素可赋值给 'number',则获取它,
// 如果找不到则返回 'never'。
type TryGetNumberIfFirst<T> =
    T extends [infer U extends number, ...unknown[]] ? U : never;

如果这些 infer 类型出现在模板字符串类型中并且被约束为原始类型,TypeScript 现在将尝试解析出一个字面量类型。

ts
// 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 解析这些字面量类型时,它会贪婪地尝试解析尽可能多的看起来像适当原始类型的内容; 然而,它会检查该原始类型的打印表示是否与字符串内容匹配。 换句话说,TypeScript 检查从字符串到原始类型再返回是否匹配。 如果它发现字符串不能“往返”,那么它将回退到基本原始类型。

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

你可以在此处查看更多关于此功能的信息

--build--watch--incremental 性能改进

TypeScript 4.8 引入了多项优化,这些优化应该会加快 --watch--incremental 场景,以及使用 --build 的项目引用构建。 例如,TypeScript 现在能够在 --watch 模式下避免在无操作更改期间更新时间戳,这使得重建更快,并且避免干扰可能正在监视 TypeScript 输出的其他构建工具。 还引入了许多其他优化,我们能够在 --build--watch--incremental 之间重用信息。

这些改进有多大? 在一个相当大的内部代码库上,我们看到在许多简单的常见操作中时间减少了 10%-25%,在无更改场景中时间减少了约 40%。 我们在 TypeScript 代码库上也看到了类似的结果。

你可以在 GitHub 上查看更改以及性能结果

比较对象和数组字面量时的错误

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

py
if people_at_home == []:
    print("here's where I lie, broken inside. </3")
    adopt_animals()

在 JavaScript 中情况并非如此,对象(因此也包括数组)之间的 ===== 检查两个引用是否指向相同的值。 我们认为 JavaScript 中类似的代码对于 JavaScript 开发者来说最多是一个早期的陷阱,最坏的情况下是生产代码中的 bug。 这就是为什么 TypeScript 现在禁止如下代码。

ts
if (peopleAtHome === []) {
//  ~~~~~~~~~~~~~~~~~~~
// 此条件将始终返回 'false',因为 JavaScript 通过引用比较对象,而不是按值。
    console.log("here's where I lie, broken inside. </3")
    adoptAnimals();
}

我们要感谢 Jack Works 贡献了此项检查。 你可以在此处查看涉及的更改

从绑定模式改进推断

在某些情况下,TypeScript 将从绑定模式中获取类型以做出更好的推断。

ts
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 有好处,但在其他情况下却不足。 例如,考虑以下代码

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

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

绑定模式 [x, y, z] 提示 f 应该产生一个 [any, any, any] 元组; 但 f 真的不应该根据绑定模式改变其类型参数。 它不能根据被赋值的内容突然产生一个新的类数组值,因此绑定模式类型对产生的类型影响过大。 最重要的是,因为绑定模式类型充满了 any,我们最终将 xyz 类型化为 any

在 TypeScript 4.8 中,这些绑定模式永远不会用作类型参数的候选。 相反,它们仅在像我们的 chooseRandomly 示例中参数需要更具体类型时才被参考。 如果你需要恢复到旧行为,你始终可以提供显式类型参数。

如果你好奇想了解更多,可以在 GitHub 上查看此更改

文件监视修复(尤其是在 git checkout 之间)

我们有一个长期存在的 bug,TypeScript 在 --watch 模式和编辑器场景中很难处理某些文件更改。 有时症状是过时或不准确的错误,需要重启 tsc 或 VS Code。 这些通常发生在 Unix 系统上,你可能在使用 vim 保存文件或在 git 中切换分支后遇到过这些问题。

这是由 Node.js 如何处理跨文件系统的重命名事件的假设引起的。 Linux 和 macOS 使用的文件系统利用 inode,而 Node.js 会将文件监视器附加到 inode 而不是文件路径。 因此,当 Node.js 返回一个监视器对象时,它可能正在监视一个路径或一个 inode,具体取决于平台和文件系统。

为了提高效率,如果 TypeScript 检测到磁盘上仍然存在某个路径,它会尝试重用相同的监视器对象。 这就是问题所在,因为即使该路径上仍然存在一个文件,也可能创建了一个不同的文件,并且该文件将具有不同的 inode。 因此,TypeScript 最终会重用监视器对象,而不是在原始位置安装一个新的监视器,并监视可能完全无关的文件的变化。 因此,TypeScript 4.8 现在在 inode 系统上处理这些情况,并正确安装新的监视器并修复此问题。

我们要感谢 Marc Celani 及其在 Airtable 的团队,他们投入大量时间调查他们遇到的问题并指出了根本原因。 你可以在此处查看有关文件监视的具体修复

查找所有引用的性能改进

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

你可以在此处阅读有关此改进的更多信息

从自动导入中排除特定文件

TypeScript 4.8 引入了一个编辑器偏好设置,用于从自动导入中排除文件。 在 Visual Studio Code 中,可以在设置 UI 中的“Auto Import File Exclude Patterns”下添加文件名或 glob 模式,或者在 .vscode/settings.json 文件中添加:

jsonc
{
    // 注意,也可以为 JavaScript 指定 `javascript.preferences.autoImportFileExcludePatterns`。
    "typescript.preferences.autoImportFileExcludePatterns": [
      "**/node_modules/@types/node"
    ]
}

这在某些情况下很有用,例如当你无法避免在编译中包含某些模块或库,但又很少想从中导入时。 这些模块可能有大量导出,会污染自动导入列表,使其更难导航,而此选项可以在这些情况下提供帮助。

你可以在此处查看实现的更多细节

正确性修复和破坏性更改

由于类型系统更改的性质,几乎不可能做出不影响某些代码的更改; 然而,有一些更改更可能需要调整现有代码。

lib.d.ts 更新

虽然 TypeScript 努力避免重大破坏,但即使内置库中的微小更改也可能导致问题。 我们预计 DOM 和 lib.d.ts 更新不会导致重大破坏,但一个值得注意的更改是 Error 上的 cause 属性现在的类型是 unknown 而不是 Error

无约束泛型不再可赋值给 {}

在 TypeScript 4.8 中,对于启用了 strictNullChecks 的项目,当无约束类型参数在 nullundefined 不是合法值的位置使用时,TypeScript 现在将正确发出错误。 这将包括任何期望 {}object 或具有全可选属性的对象类型的类型。

一个简单的示例可以在下面看到。

ts
// 接受任何非空非 undefined 的值
function bar(value: {}) {
  Object.keys(value); // 此调用在运行时对 null/undefined 抛出异常。
}

// 无约束类型参数 T...
function foo<T>(x: T) {
    bar(x); // 以前允许,现在在 4.8 中是错误。
    //  ~
    // 错误:类型 'T' 的参数不能赋给类型 '{}' 的参数。
}

foo(undefined);

如上所示,这样的代码存在潜在 bug——值 nullundefined 可以通过这些无约束类型参数间接传递给不应该观察这些值的代码。

此行为也将在类型位置可见。一个示例是:

ts
interface Foo<T> {
  x: Bar<T>;
}

interface Bar<T extends {}> { }

不希望处理 nullundefined 的现有代码可以通过传播适当的约束来修复。

diff
- function foo<T>(x: T) {
+ function foo<T extends {}>(x: T) {

另一种解决方法是在运行时检查 nullundefined

diff
  function foo<T>(x: T) {
+     if (x !== null && x !== undefined) {
          bar(x);
+     }
  }

如果你知道由于某些原因,你的泛型值不可能是 nullundefined,你可以直接使用非空断言。

diff
  function foo<T>(x: T) {
-     bar(x);
+     bar(x!);
  }

在类型方面,你通常要么需要传播约束,要么将你的类型与 {} 相交。

有关更多信息,你可以查看引入此更改的更改以及关于无约束泛型现在如何工作的具体讨论问题

装饰器被放置在 TypeScript 语法树的 modifiers

TC39 中装饰器的当前方向意味着 TypeScript 将不得不处理装饰器放置位置的破坏性变化。 以前,TypeScript 假设装饰器总是放置在所有关键字/修饰符之前。 例如

ts
@decorator
export class Foo {
  // ...
}

当前提议的装饰器不支持此语法。 相反,export 关键字必须位于装饰器之前。

ts
export @decorator class Foo {
  // ...
}

不幸的是,TypeScript 的树是具体的而不是抽象的,我们的架构期望语法树节点字段完全按顺序排列。 为了同时支持遗留装饰器和提议的装饰器,TypeScript 将不得不优雅地解析和交错修饰符和装饰器。

为此,它公开了一个新的类型别名 ModifierLike,它是 ModifierDecorator

ts
export type ModifierLike = Modifier | Decorator;

装饰器现在被放置在 modifiers 字段中,该字段在设置时现在是一个 NodeArray<ModifierLike>,并且整个字段已被弃用。

diff
- readonly modifiers?: NodeArray<Modifier> | undefined;
+ /**
+  * @deprecated ...
+  * 使用 `ts.canHaveModifiers()` 来测试 `Node` 是否可以有修饰符。
+  * 使用 `ts.getModifiers()` 来获取 `Node` 的修饰符。
+  * ...
+  */
+ readonly modifiers?: NodeArray<ModifierLike> | undefined;

所有现有的 decorators 属性已被标记为已弃用,如果读取将始终是 undefined。 该类型也已更改为 undefined,以便现有工具知道如何正确处理它们。

diff
- readonly decorators?: NodeArray<Decorator> | undefined;
+ /**
+  * @deprecated ...
+  * 使用 `ts.canHaveDecorators()` 来测试 `Node` 是否可以有装饰器。
+  * 使用 `ts.getDecorators()` 来获取 `Node` 的装饰器。
+  * ...
+  */
+ readonly decorators?: undefined;

为了避免新的弃用警告和其他问题,TypeScript 现在公开了四个新函数来代替 decoratorsmodifiers 属性。 有单独的谓词用于测试节点是否支持修饰符和装饰器,以及用于获取它们的相应访问器函数。

ts
function canHaveModifiers(node: Node): node is HasModifiers;
function getModifiers(node: HasModifiers): readonly Modifier[] | undefined;

function canHaveDecorators(node: Node): node is HasDecorators;
function getDecorators(node: HasDecorators): readonly Decorator[] | undefined;

例如,如何访问节点的修饰符,你可以编写

ts
const modifiers = canHaveModifiers(myNode) ? getModifiers(myNode) : undefined;

需要注意的是,每次调用 getModifiersgetDecorators 可能会分配一个新数组。

有关更多信息,请查看关于以下内容的更改:

类型不能在 JavaScript 文件中导入/导出

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

ts
// @ts-check

// 将在运行时失败,因为 'SomeType' 不是一个值。
import { someValue, SomeType } from "some-module";

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

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

// 将在运行时失败,因为 'MyType' 不是一个值。
export { MyType as MyExportedType };

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

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

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

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

+ /**
+  * @typedef {MyType} MyExportedType
+  */
- export { MyType as MyExportedType };

你可以在此处阅读更多关于此更改的信息

绑定模式不直接贡献于推断候选

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

绑定模式中未使用的重命名现在在类型签名中是错误

TypeScript 的类型注解语法通常看起来像是在解构值时可以使用。 例如,考虑以下函数。

ts
declare function makePerson({ name: string, age: number }): Person;

你可能会阅读此签名并认为 makePerson 显然接受一个具有 name 属性(类型为 string)和 age 属性(类型为 number)的对象; 然而,JavaScript 的解构语法实际上在这里优先。 makePerson 确实表示它将接受一个具有 nameage 属性的对象,但不是为它们指定类型,它只是说它将 nameage 分别重命名为 stringnumber

在纯类型构造中,编写这样的代码是无用的,并且通常是一个错误,因为开发者通常认为他们在编写类型注解。

TypeScript 4.8 使这些成为错误,除非它们在签名后面被引用。 编写上述签名的正确方法如下:

ts
declare function makePerson(options: { name: string, age: number }): Person;

// 或

declare function makePerson({ name, age }: { name: string, age: number }): Person;

此更改可以捕获声明中的 bug,并有助于改进现有代码。 我们要感谢 GitHub 用户 uhyo 提供了此项检查。 你可以在此处阅读有关此更改的信息