TypeScript 5.3

导入属性

🌐 Import Attributes

TypeScript 5.3 支持 import 属性 提案的最新更新。

🌐 TypeScript 5.3 supports the latest updates to the import attributes proposal.

导入属性的一个用例是向运行时提供有关模块预期格式的信息。

🌐 One use-case of import attributes is to provide information about the expected format of a module to the runtime.

ts
// 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 不会检查它们的内容,因此它们只是被保留下来,以便浏览器和运行时能够处理它们(并且可能出现错误)。

🌐 The contents of these attributes are not checked by TypeScript since they’re host-specific, and are simply left alone so that browsers and runtimes can handle them (and possibly error).

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

动态 import() 调用也可以通过第二个参数使用导入属性。

🌐 Dynamic import() calls can also use import attributes through a second argument.

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

第二个参数的预期类型由一个名为 ImportCallOptions 的类型定义,默认情况下,它只期望一个名为 with 的属性。

🌐 The expected type of that second argument is defined by a type called ImportCallOptions, which by default just expects a property called with.

请注意,导入属性(import attributes)是早期提案“导入断言”(import assertions)的演变,该提案在 TypeScript 4.5 中得到了实现。最明显的区别是使用 with 关键字而不是 assert 关键字。但不太显而易见的区别在于,运行时现在可以自由使用属性来指导导入路径的解析和解释,而导入断言只能在加载模块后断言某些特性。

🌐 Note that import attributes are an evolution of an earlier proposal called “import assertions”, which were implemented in TypeScript 4.5. The most obvious difference is the use of the with keyword over the assert keyword. But the less-visible difference is that runtimes are now free to use attributes to guide the resolution and interpretation of import paths, whereas import assertions could only assert some characteristics after loading a module.

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

🌐 Over time, TypeScript will be deprecating the old syntax for import assertions in favor of the proposed syntax for import attributes. Existing code using assert should migrate towards the with keyword. New code that needs an import attribute should use with exclusively.

我们想感谢 Oleksandr Tarasiuk 实现了这个提案! 我们也想表扬 Wenlu Wang导入断言 的实现!

🌐 We’d like to thank Oleksandr Tarasiuk for implementing this proposal! And we’d also like to call out Wenlu Wang for their implementation of import assertions!

导入类型中的稳定支持 resolution-mode

🌐 Stable Support resolution-mode in Import Types

在 TypeScript 4.7 中,TypeScript 为 /// <reference types="..." /> 添加了对 resolution-mode 属性的支持,以控制应使用 import 还是 require 语义解析指定符。

🌐 In TypeScript 4.7, TypeScript added support for a resolution-mode attribute in /// <reference types="..." /> to control whether a specifier should be resolved via import or require semantics.

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

在仅类型导入中也添加了一个对应的字段以导入断言;然而,它仅在 TypeScript 的夜间版本中受支持。其原理是,从本质上讲,导入断言并不是为了指导模块解析。因此,该功能以仅限夜间版本的实验模式发布,以获得更多反馈。

🌐 A corresponding field was added to import assertions on type-only imports as well; however, it was only supported in nightly versions of TypeScript. The rationale was that in spirit, import assertions were not intended to guide module resolution. So this feature was shipped experimentally in a nightly-only mode to get more feedback.

但鉴于 导入属性 可以指导解析,并且我们已经看到了一些合理的使用案例,TypeScript 5.3 现在支持 import typeresolution-mode 属性。

🌐 But given that import attributes can guide resolution, and that we’ve seen reasonable use-cases, TypeScript 5.3 now supports the resolution-mode attribute for import type.

ts
// 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() 类型。

🌐 These import attributes can also be used on import() types.

ts
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 {}

欲了解更多信息,请查看此处的更改

🌐 For more information, check out the change here

resolution-mode 支持所有模块模式

🌐 resolution-mode Supported in All Module Modes

以前,只有在 moduleResolution 选项 node16nodenext 下才允许使用 resolution-mode。 为了更方便地查找专门用于类型的模块,resolution-mode 现在在其他所有 moduleResolution 选项(如 bundlernode10)下都能正常工作,并且在 classic 下不会出错。

🌐 Previously, using resolution-mode was only allowed under the moduleResolution options node16 and nodenext. To make it easier to look up modules specifically for type purposes, resolution-mode now works appropriately in all other moduleResolution options like bundler, node10, and simply doesn’t error under classic.

欲了解更多信息,请参阅实现该功能的拉取请求

🌐 For more information, see the implementing pull request.

switch (true) 收窄

🌐 switch (true) Narrowing

TypeScript 5.3 现在可以根据 switch (true) 中每个 case 子句的条件进行类型缩小。

🌐 TypeScript 5.3 now can perform narrowing based on conditions in each case clause within a switch (true).

ts
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 发起并领导 initial work。我们想对这一贡献表示“感谢!”

通过布尔值比较缩小范围

🌐 Narrowing On Comparisons to Booleans

有时你可能会发现在条件中直接将 truefalse 进行比较。 通常这些比较是不必要的,但你可能出于风格上的考虑,或为了避免 JavaScript 中与真值相关的某些问题而使用它。 无论如何,以前 TypeScript 在进行类型缩小时并不识别这种形式。

🌐 Occasionally you may find yourself performing a direct comparison with true or false in a condition. Usually these are unnecessary comparisons, but you might prefer it as a point of style, or to avoid certain issues around JavaScript truthiness. Regardless, previously TypeScript just didn’t recognize such forms when performing narrowing.

TypeScript 5.3 现在在范围缩小变量时能够跟上并理解这些表达式。

🌐 TypeScript 5.3 now keeps up and understands these expressions when narrowing variables.

ts
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 提交了实现此功能的 拉取请求

🌐 We’d like to thank Mateusz Burzyński for the pull request that implemented this.

instanceof 通过 Symbol.hasInstance 缩小

🌐 instanceof Narrowing Through Symbol.hasInstance

JavaScript 的一个略显深奥的特性是可以重写 instanceof 操作符的行为。要实现这一点,instanceof 操作符右侧的值需要有一个名为 Symbol.hasInstance 的特定方法。

🌐 A slightly esoteric feature of JavaScript is that it is possible to override the behavior of the instanceof operator. To do so, the value on the right side of the instanceof operator needs to have a specific method named by Symbol.hasInstance.

js
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 运算符左侧的被测试值将会被该类型谓词适当地缩小类型。

🌐 To better model this behavior in instanceof, TypeScript now checks if such a [Symbol.hasInstance] method exists and is declared as a type predicate function. If it does, the tested value on the left side of the instanceof operator will be narrowed appropriately by that type predicate.

ts
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

🌐 As you can see in this example, Point defines its own [Symbol.hasInstance] method. It actually acts as a custom type guard over a separate type called PointLike. In the function f, we were able to narrow value down to a PointLike with instanceof, but not a Point. That means that we can access the properties x and y, but not the method distanceFromOrigin.

欲了解更多信息,你可以在这里阅读有关此变更的内容

🌐 For more information, you can read up on this change here.

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

🌐 Checks for super Property Accesses on Instance Fields

在 JavaScript 中,可以通过 super 关键字访问基类中的声明。

🌐 In JavaScript, it’s possible to access a declaration in a base class through the super keyword.

js
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() 的东西,因为那可能会调用被重写的方法。这是一个微妙的区别,更微妙的是,如果某个声明根本没有被重写,这两者通常可以互换使用。

🌐 This is different from writing something like this.someMethod(), since that could invoke an overridden method. This is a subtle distinction, made more subtle by the fact that often the two can be interchangeable if a declaration is never overridden at all.

js
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 被定义为字段,你会遇到运行时错误!

🌐 The problem is using them interchangeably is that super only works on members declared on the prototype — not instance properties. That means that if you wrote super.someMethod(), but someMethod was defined as a field, you’d get a runtime error!

ts
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 属性的访问或方法调用,以确定它们是否对应类字段。如果是,我们现在会得到类型检查错误。

🌐 TypeScript 5.3 now more-closely inspects super property accesses/method calls to see if they correspond to class fields. If they do, we’ll now get a type-checking error.

此支票 的贡献要感谢 Jack Works

类型的交互式嵌入提示

🌐 Interactive Inlay Hints for Types

TypeScript 的内联提示现在支持跳转到类型的定义!这使得随意浏览代码变得更加容易。

🌐 TypeScript’s inlay hints now support jumping to the definition of types! This makes it easier to casually navigate your code.

Ctrl-clicking an inlay hint to jump to the definition of a parameter type.

在此查看更多内容 实现方法在这里

🌐 See more at the implementation here.

设置以偏好 type 自动导入

🌐 Settings to Prefer type Auto-Imports

以前,当 TypeScript 为类型位置的某些内容生成自动导入时,它会根据你的设置添加 type 修饰符。例如,当在以下内容中对 Person 获取自动导入时:

🌐 Previously when TypeScript generated auto-imports for something in a type position, it would add a type modifier based on your settings. For example, when getting an auto-import on Person in the following:

ts
export let p: Person

TypeScript 的编辑体验通常会将 Person 导入添加为:

🌐 TypeScript’s editing experience would usually add an import for Person as:

ts
import { Person } from "./types";
export let p: Person

在某些设置下,例如 verbatimModuleSyntax,它会添加 type 修饰符:

🌐 and under certain settings like verbatimModuleSyntax, it would add the type modifier:

ts
import { type Person } from "./types";
export let p: Person

然而,也许你的代码库无法使用其中的一些选项;或者你只是偏好在可能的情况下使用明确的 type 导入。

🌐 However, maybe your codebase isn’t able to use some of these options; or you just have a preference for explicit type imports when possible.

随着最近的更改,TypeScript 现在将此功能作为编辑器特定的选项启用。在 Visual Studio Code 中,你可以在界面中启用它,路径为“TypeScript › Preferences: Prefer Type Only Auto Imports”,或者通过 JSON 配置选项 typescript.preferences.preferTypeOnlyAutoImports

通过跳过 JSDoc 解析进行优化

🌐 Optimizations by Skipping JSDoc Parsing

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

🌐 When running TypeScript via tsc, the compiler will now avoid parsing JSDoc. This drops parsing time on its own, but also reduces memory usage to store comments along with time spent in garbage collection. All-in-all, you should see slightly faster compiles and quicker feedback in --watch mode.

具体更改可以在这里查看

由于并非使用 TypeScript 的每个工具都需要存储 JSDoc(例如 typescript-eslint 和 Prettier),因此这种解析策略已作为 API 本身的一部分提供。这可以使这些工具获得我们为 TypeScript 编译器带来的相同内存和速度改进。注释解析策略的新选项在 JSDocParsingMode 中进行了说明。更多信息请参见 此拉取请求

🌐 Because not every tool using TypeScript will need to store JSDoc (e.g. typescript-eslint and Prettier), this parsing strategy has been surfaced as part of the API itself. This can enable these tools to gain the same memory and speed improvements we’ve brought to the TypeScript compiler. The new options for comment parsing strategy are described in JSDocParsingMode. More information is available on this pull request.

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

🌐 Optimizations by Comparing Non-Normalized Intersections

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

🌐 In TypeScript, unions and intersections always follow a specific form, where intersections can’t contain union types. That means that when we create an intersection over a union like A & (B | C), that intersection will be normalized into (A & B) | (A & C). Still, in some cases the type system will maintain the original form for display purposes.

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

🌐 It turns out that the original form can be used for some clever fast-path comparisons between types.

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

🌐 For example, let’s say we have SomeType & (Type1 | Type2 | ... | Type99999NINE) and we want to see if that’s assignable to SomeType. Recall that we don’t really have an intersection as our source type — we have a union that looks like (SomeType & Type1) | (SomeType & Type2) | ... |(SomeType & Type99999NINE). When checking if a union is assignable to some target type, we have to check if every member of the union is assignable to the target type, and that can be very slow.

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

🌐 In TypeScript 5.3, we peek at the original intersection form that we were able to tuck away. When we compare the types, we do a quick check to see if the target exists in any constituent of the source intersection.

欲了解更多信息,请参阅此拉取请求(链接)。

🌐 For more information, see this pull request.

tsserverlibrary.jstypescript.js 的整合

🌐 Consolidation Between tsserverlibrary.js and typescript.js

TypeScript 本身提供两个库文件:tsserverlibrary.jstypescript.js。 某些 API 仅在 tsserverlibrary.js 中可用(例如 ProjectService API),这对一些导入者可能很有用。 然而,这两者是不同的包,并且有大量重叠内容,导致包中代码重复。 更重要的是,由于自动导入或使用习惯,要始终使用其中一个可能很具挑战性。 不小心同时加载两个模块太容易了,代码在 API 的不同实例上可能无法正常工作。 即使代码可以工作,加载第二个包也会增加资源使用。

🌐 TypeScript itself ships two library files: tsserverlibrary.js and typescript.js. There are certain APIs available only in tsserverlibrary.js (like the ProjectService API), which may be useful to some importers. Still, the two are distinct bundles with a lot of overlap, duplicating code in the package. What’s more, it can be challenging to consistently use one over the other due to auto-imports or muscle memory. Accidentally loading both modules is far too easy, and code may not work properly on a different instance of the API. Even if it does work, loading a second bundle increases resource usage.

鉴于此,我们决定将两者合并。 typescript.js 现在包含了 tsserverlibrary.js 曾经包含的内容,而 tsserverlibrary.js 现在只是简单地重新导出 typescript.js。 比较合并前后的情况,我们看到包大小有如下减少:

🌐 Given this, we’ve decided to consolidate the two. typescript.js now contains what tsserverlibrary.js used to contain, and tsserverlibrary.js now simply re-exports typescript.js. Comparing the before/after of this consolidation, we saw the following reduction in package size:

之前 之后 差异 差异(百分比)
压缩后 6.90 MiB 5.48 MiB -1.42 MiB -20.61%
解压后 38.74 MiB 30.41 MiB -8.33 MiB -21.50%
之前 之后 差异 差异(百分比)
lib/tsserverlibrary.d.ts 570.95 KiB 865.00 B -570.10 KiB -99.85%
lib/tsserverlibrary.js 8.57 MiB 1012.00 B -8.57 MiB -99.99%
lib/typescript.d.ts 396.27 KiB 570.95 KiB +174.68 KiB +44.08%
lib/typescript.js 7.95 MiB 8.57 MiB +637.53 KiB +7.84%

换句话说,这将包大小减少 20.5% 以上。

🌐 In other words, this is over a 20.5% reduction in package size.

欲了解更多信息,你可以查看这里的工作内容

🌐 For more information, you can see the work involved here.

重大变更和正确性改进

🌐 Breaking Changes and Correctness Improvements

lib.d.ts 变更

🌐 lib.d.ts Changes

为 DOM 生成的类型可能会影响你的代码库。有关更多信息,请参阅 TypeScript 5.3 的 DOM 更新

🌐 Types generated for the DOM may have an impact on your codebase. For more information, see the DOM updates for TypeScript 5.3.

检查实例属性上的 super 访问

🌐 Checks for super Accesses on Instance Properties

TypeScript 5.3 现在可以检测到通过 super. 属性访问引用的声明是否是类字段,并会发出错误提示。这可以防止在运行时可能发生的错误。

🌐 TypeScript 5.3 now detects when the declaration referenced by a super. property access is a class field and issues an error. This prevents errors that might occur at runtime.

查看更多有关此更改的信息