TypeScript 5.3

导入属性(Import Attributes)

TypeScript 5.3 支持了最新的 import attributes 提案。

该特性的一个用例是为运行时提供期望的模块格式信息。

// We only want this to be interpreted as JSON,
// not a runnable/malicious JavaScript file with a `.json` extension.
import obj from "./something.json" with { type: "json" };

TypeScript 不会检查属性内容,因为它们是宿主环境相关的。 TypeScript 会原样保留它们,浏览器和运行时会处理它们。

// TypeScript is fine with this.
// But your browser? Probably not.
import * as foo from "./foo.js" with { type: "fluffy bunny" };

动态的 import() 调用也可以在第二个参数里使用该特性。

const obj = await import('./something.json', {
  with: { type: 'json' },
});

第二个参数的期望类型为 ImportCallOptions,默认只支持一个名为 with 的属性。

请注意,导入属性是之前提案“导入断言”的演进,该提案已在 TypeScript 4.5 中实现。 最明显的区别是使用with关键字而不是assert关键字。 但不太明显的区别是,现在运行时可以自由地使用属性来指导导入路径的解析和解释,而导入断言只能在加载模块后断言某些特性。

随着时间的推移,TypeScript 将逐渐弃用旧的导入断言语法,转而采用导入属性的提议语法。现有的使用assert的代码应该迁移到with关键字。而需要导入属性的新代码应该完全使用with关键字。

感谢 Oleksandr Tarasiuk 实现了这个功能! 也感谢 Wenlu Wang 实现了 import assertions!

稳定支持 import type 上的 resolution-mode

TypeScript 4.7 在 /// <reference types="..." /> 里支持了 resolution-mode 属性, 它用来控制一个描述符是使用 import 还是 require 语义来解析。

/// <reference types="pkg" resolution-mode="require" />

// or

/// <reference types="pkg" resolution-mode="import" />

在 type-only 导入上,导入断言也引入了相应的字段; 然而,它仅在 TypeScript 的夜间版本中得到支持 其原因是在精神上,导入断言并不打算指导模块解析。 因此,这个特性以实验性的方式仅在夜间版本中发布,以获得更多的反馈。

但是,导入属性(Import Attributes)可以指导解析,并且我们也已经看到了有意义的用例, TypeScript 5.3 在 import type 上支持了 resolution-mode

// Resolve `pkg` as if we were importing with a `require()`
import type { TypeFromRequire } from "pkg" with {
    "resolution-mode": "require"
};

// Resolve `pkg` as if we were importing with an `import`
import type { TypeFromImport } from "pkg" with {
    "resolution-mode": "import"
};

export interface MergedType extends TypeFromRequire, TypeFromImport {}

这些导入属性也可以用在 import() 类型上。

export type TypeFromRequire =
    import("pkg", { with: { "resolution-mode": "require" } }).TypeFromRequire;

export type TypeFromImport =
    import("pkg", { with: { "resolution-mode": "import" } }).TypeFromImport;

export interface MergedType extends TypeFromRequire, TypeFromImport {}

更多详情,请参考PR

在所有模块模式中支持 resolution-mode

此前,仅在 moduleResolutionnode16nodenext 时支持 resolution-mode。 为了使查找模块更容易,尤其针对类型,resolution-mode 现在可以在所有其它的 moduleResolution 选项下工作,例如 bundlernode10,甚至在 classic 下也不报错。

更多详情,请参考PR

switch (true) 类型细化

TypeScript 5.3 会针对 switch (true) 里的每一个 case 条件进行类型细化。

function f(x: unknown) {
  switch (true) {
    case typeof x === 'string':
      // 'x' is a 'string' here
      console.log(x.toUpperCase());
    // falls through...

    case Array.isArray(x):
      // 'x' is a 'string | any[]' here.
      console.log(x.length);
    // falls through...

    default:
    // 'x' is 'unknown' here.
    // ...
  }
}

感谢 Mateusz Burzyński 的贡献

类型细化与布尔值的比较

有时,您可能会发现自己在条件语句中直接与 truefalse 进行比较。 通常情况下,这些比较是不必要的,但您可能出于风格上的考虑或为了避免 JavaScript 中真值相关的某些问题而偏好这样做。 不过,之前 TypeScript 在进行类型细化时并不识别这样的形式。

TypeScript 5.3 在类型细化时可以理解这类表达式。

interface A {
  a: string;
}

interface B {
  b: string;
}

type MyType = A | B;

function isA(x: MyType): x is A {
  return 'a' in x;
}

function someFn(x: MyType) {
  if (isA(x) === true) {
    console.log(x.a); // works!
  }
}

感谢 Mateusz Burzyński 的 PR

利用 Symbol.hasInstance 来细化 instanceof

JavaScript 的一个稍微晦涩的特性是可以覆盖 instanceof 运算符的行为。 为此,instanceof 运算符右侧的值需要具有一个名为 Symbol.hasInstance 的特定方法。

class Weirdo {
  static [Symbol.hasInstance](testedValue) {
    // wait, what?
    return testedValue === undefined;
  }
}

// false
console.log(new Thing() instanceof Weirdo);

// true
console.log(undefined instanceof Weirdo);

为了更好地支持 instanceof 的行为,TypeScript 现在会检查是否存在 [Symbol.hasInstance] 方法且被定义为类型判定函数。 如果有的话,instanceof 运算符左侧的值会按照类型判定进行细化。

interface PointLike {
  x: number;
  y: number;
}

class Point implements PointLike {
  x: number;
  y: number;

  constructor(x: number, y: number) {
    this.x = x;
    this.y = y;
  }

  distanceFromOrigin() {
    return Math.sqrt(this.x ** 2 + this.y ** 2);
  }

  static [Symbol.hasInstance](val: unknown): val is PointLike {
    return (
      !!val &&
      typeof val === 'object' &&
      'x' in val &&
      'y' in val &&
      typeof val.x === 'number' &&
      typeof val.y === 'number'
    );
  }
}

function f(value: unknown) {
  if (value instanceof Point) {
    // Can access both of these - correct!
    value.x;
    value.y;

    // Can't access this - we have a 'PointLike',
    // but we don't *actually* have a 'Point'.
    value.distanceFromOrigin();
  }
}

能够看到例子中,Point 定义了自己的 [Symbol.hasInstance] 方法。 它实际上充当了对称为 PointLike 的单独类型的自定义类型保护。 在函数 f 中,我们能够使用 instanceofvalue 细化为 PointLike,但不能细化到 Point。 这意味着我们可以访问属性 xy,但无法访问 distanceFromOrigin 方法。

更多详情请参考PR

在实例字段上检查 super 属性访问

在 JavaScript 中,能够使用 super 关键字来访问基类中的声明。

class Base {
  someMethod() {
    console.log('Base method called!');
  }
}

class Derived extends Base {
  someMethod() {
    console.log('Derived method called!');
    super.someMethod();
  }
}

new Derived().someMethod();
// Prints:
//   Derived method called!
//   Base method called!

这与 this.someMethod() 是不同的,因为它可能调用的是重写的方法。 这是一个微妙的区别,而且通常情况下,如果一个声明从未被覆盖,这两者可以互换,使得区别更加微妙。

class Base {
  someMethod() {
    console.log('someMethod called!');
  }
}

class Derived extends Base {
  someOtherMethod() {
    // These act identically.
    this.someMethod();
    super.someMethod();
  }
}

new Derived().someOtherMethod();
// Prints:
//   someMethod called!
//   someMethod called!

将它们互换使用的问题在于,super 关键字仅适用于在原型上声明的成员,而不适用于实例属性。 这意味着,如果您编写了 super.someMethod(),但 someMethod 被定义为一个字段,那么您将会得到一个运行时错误!

class Base {
  someMethod = () => {
    console.log('someMethod called!');
  };
}

class Derived extends Base {
  someOtherMethod() {
    super.someMethod();
  }
}

new Derived().someOtherMethod();
//
// Doesn't work because 'super.someMethod' is 'undefined'.

TypeScript 5.3 现在更仔细地检查 super 属性访问/方法调用,以确定它们是否对应于类字段。 如果是这样,我们现在将会得到一个类型检查错误。

这个检查是由 Jack Works 开发!

可以交互的类型内嵌提示

TypeScript 的内嵌提示支持跳转到类型定义! 这便利在代码间跳转变得简单。

更多详情请参考PR

设置偏好 type 自动导入

之前,当 TypeScript 为类型自动生成导入语句时,它会根据配置添加 type 修饰符。 例如,当为 Person 生成自动导入语句时:

export let p: Person;

TypeScript 通常会这样生成 Person 导入:

import { Person } from './types';

export let p: Person;

如果设置了 verbatimModuleSyntax,它会添加 type 修饰符:

import { type Person } from './types';

export let p: Person;

然而,也许你的编辑器不支持这些选项;或者你偏好显式地使用 type 导入。

最近的一项改动,TypeScript 把它变成了针对编辑器的配置项。 在 Visual Studio Code 中,你可以在 "TypeScript › Preferences: Prefer Type Only Auto Imports" 启用该功能,或者在 JSON 配置文件中的 typescript.preferences.preferTypeOnlyAutoImports 设置。

优化:略过 JSDoc 解析

当通过 tsc 运行 TypeScript 时,编译器现在将避免解析 JSDoc。 这不仅减少了解析时间,还减少了存储注释以及垃圾回收所花费的内存使用量。 总体而言,您应该会看到编译速度稍微更快,并在 --watch 模式下获得更快的反馈。

具体改动在这

由于并非每个使用 TypeScript 的工具都需要存储 JSDoc(例如 typescript-eslint 和 Prettier),因此这种解析策略已作为 API 的一部分公开。 这使得这些工具能够获得与 TypeScript 编译器相同的内存和速度改进。 注释解析策略的新选项在 JSDocParsingMode 中进行了描述。 关于此拉取请求的更多信息,请参阅PR

通过比较非规范化的交叉类型进行优化

在 TypeScript 中,联合类型和交叉类型始终遵循特定的形式,其中交叉类型不能包含联合类型。 这意味着当我们在一个联合类型上创建一个交叉类型,例如 A & (B | C),该交叉类型将被规范化为 (A & B) | (A & C)。 然而,在某些情况下,类型系统会保留原始形式以供显示目的使用。

事实证明,原始形式可以用于一些巧妙的快速路径类型比较。

例如,假设我们有 SomeType & (Type1 | Type2 | ... | Type99999NINE),我们想要确定它是否可以赋值给 SomeType。 回想一下,我们实际上没有一个交叉类型作为源类型,而是一个联合类型,看起来像是 (SomeType & Type1) | (SomeType & Type2) | ... | (SomeType & Type99999NINE)。 当检查一个联合类型是否可以赋值给目标类型时,我们必须检查联合类型的每个成员是否可以赋值给目标类型,这可能非常慢。

在 TypeScript 5.3 中,我们查看了我们能够隐藏的原始交叉类型形式。 当我们比较这些类型时,我们会快速检查目标类型是否存在于源交叉类型的任何组成部分中。

更多详情请参考PR

合并 tsserverlibrary.jstypescript.js

TypeScript 本身包含两个库文件:tsserverlibrary.jstypescript.js。 在 tsserverlibrary.js 中有一些仅在其中可用的 API(如 ProjectService API),对某些导入者可能很有用。 尽管如此,这两个是不同的捆绑包,有很多重叠的部分,在包中重复了一些代码。 更重要的是,由于自动导入或肌肉记忆的原因,要始终一致地使用其中一个可能是具有挑战性的。 意外加载两个模块太容易了,而且代码可能在 API 的不同实例上无法正常工作。 即使它可以工作,加载第二个捆绑包会增加资源使用量。

基于此,我们决定合并这两个文件。 typescript.js 现在包含了以前在 tsserverlibrary.js 中的内容,而 tsserverlibrary.js 现在只是重新导出 typescript.js。 在这个合并前后,我们看到了以下包大小的减小:

BeforeAfterDiffDiff (percent)
Packed6.90 MiB5.48 MiB-1.42 MiB-20.61%
Unpacked38.74 MiB30.41 MiB-8.33 MiB-21.50%
BeforeAfterDiffDiff (percent)
lib/tsserverlibrary.d.ts570.95 KiB865.00 B-570.10 KiB-99.85%
lib/tsserverlibrary.js8.57 MiB1012.00 B-8.57 MiB-99.99%
lib/typescript.d.ts396.27 KiB570.95 KiB+174.68 KiB+44.08%
lib/typescript.js7.95 MiB8.57 MiB+637.53 KiB+7.84%

换句话说,这意味着包大小减小了超过 20.5%。

更多详情请参考 PR