TypeScript 4.7

Node.js 中的 ECMAScript 模块支持

¥ECMAScript Module Support in Node.js

过去几年,Node.js 一直致力于支持 ECMAScript 模块 (ESM)。这是一个非常棘手的功能,因为 Node.js 生态系统建立在一个名为 CommonJS (CJS) 的不同模块系统上。两者之间的互操作带来了巨大的挑战,需要处理许多新功能;但是,Node.js 对 ESM 的支持主要在 Node.js 12 及更高版本中实现。在 TypeScript 4.5 左右,我们在 Node.js 中推出了对 ESM 的夜间支持,以收集一些用户的反馈,并让库作者为更广泛的支持做好准备。

¥For the last few years, Node.js has been working to support ECMAScript modules (ESM). This has been a very difficult feature, since the Node.js ecosystem is built on a different module system called CommonJS (CJS). Interoperating between the two brings large challenges, with many new features to juggle; however, support for ESM in Node.js was largely implemented in Node.js 12 and later. Around TypeScript 4.5 we rolled out nightly-only support for ESM in Node.js to get some feedback from users and let library authors ready themselves for broader support.

TypeScript 4.7 通过两个新的 module 设置添加了此功能:node16nodenext

¥TypeScript 4.7 adds this functionality with two new module settings: node16 and nodenext.

jsonc
{
"compilerOptions": {
"module": "node16",
}
}

这些新模式带来了一些高级功能,我们将在这里进行探讨。

¥These new modes bring a few high-level features which we’ll explore here.

package.json 和新扩展中的 type

¥type in package.json and New Extensions

Node.js 支持名为 typepackage.json 中的新设置"type" 可以设置为 "module""commonjs"

¥Node.js supports a new setting in package.json called type. "type" can be set to either "module" or "commonjs".

jsonc
{
"name": "my-package",
"type": "module",
"//": "...",
"dependencies": {
}
}

此设置控制 .js.d.ts 文件是被解释为 ES 模块还是 CommonJS 模块,未设置时默认为 CommonJS。当一个文件被视为 ES 模块时,与 CommonJS 相比,有一些不同的规则:

¥This setting controls whether .js and .d.ts files are interpreted as ES modules or CommonJS modules, and defaults to CommonJS when not set. When a file is considered an ES module, a few different rules come into play compared to CommonJS:

  • 可以使用 import/export 语句。

    ¥import/export statements can be used.

  • 可以使用顶层 await

    ¥Top-level await can be used

  • 相对导入路径需要完整扩展(我们必须写成 import "./foo.js" 而不是 import "./foo")。

    ¥Relative import paths need full extensions (we have to write import "./foo.js" instead of import "./foo").

  • 导入的解析方式可能与 node_modules 中的依赖不同。

    ¥Imports might resolve differently from dependencies in node_modules.

  • 某些类似全局的值,例如 requiremodule,不能直接使用。

    ¥Certain global-like values like require and module cannot be used directly.

  • CommonJS 模块需要遵循某些特殊规则导入。

    ¥CommonJS modules get imported under certain special rules.

我们将回顾其中的一些功能。

¥We’ll come back to some of these.

为了在此系统中覆盖 TypeScript 的工作方式,.ts.tsx 文件现在的工作方式相同。当 TypeScript 找到 .ts.tsx.js.jsx 文件时,它会查找 package.json 文件以查看该文件是否为 ES 模块,并以此确定:

¥To overlay the way TypeScript works in this system, .ts and .tsx files now work the same way. When TypeScript finds a .ts, .tsx, .js, or .jsx file, it will walk up looking for a package.json to see whether that file is an ES module, and use that to determine:

  • 如何查找该文件导入的其他模块

    ¥how to find other modules which that file imports

  • 以及如何在生成输出时转换该文件

    ¥and how to transform that file if producing outputs

.ts 文件被编译为 ES 模块时,ECMAScript import/export 语句在 .js 输出中将保持不变;当它被编译为 CommonJS 模块时,它将产生与现在在 --module commonjs 下相同的输出。

¥When a .ts file is compiled as an ES module, ECMAScript import/export statements are left alone in the .js output; when it’s compiled as a CommonJS module, it will produce the same output you get today under --module commonjs.

这也意味着 ES 模块和 CJS 模块的 .ts 文件之间的路径解析方式不同。例如,假设你今天有以下代码:

¥This also means paths resolve differently between .ts files that are ES modules and ones that are CJS modules. For example, let’s say you have the following code today:

ts
// ./foo.ts
export function helper() {
// ...
}
// ./bar.ts
import { helper } from "./foo"; // only works in CJS
helper();

此代码在 CommonJS 模块中有效,但在 ES 模块中会失败,因为相对导入路径需要使用扩展。因此,必须重写它才能使用 foo.ts 输出的扩展。 - 因此 bar.ts 必须从 ./foo.js 导入。

¥This code works in CommonJS modules, but will fail in ES modules because relative import paths need to use extensions. As a result, it will have to be rewritten to use the extension of the output of foo.ts - so bar.ts will instead have to import from ./foo.js.

ts
// ./bar.ts
import { helper } from "./foo.js"; // works in ESM & CJS
helper();

乍一看,这可能感觉有点麻烦,但 TypeScript 工具(例如自动导入和路径补全)通常会为你完成这些工作。

¥This might feel a bit cumbersome at first, but TypeScript tooling like auto-imports and path completion will typically just do this for you.

另外需要提到的是,这也适用于 .d.ts 文件。当 TypeScript 在包中找到 .d.ts 文件时,它会根据包含它的包进行解释。

¥One other thing to mention is the fact that this applies to .d.ts files too. When TypeScript finds a .d.ts file in a package, it is interpreted based on the containing package.

新的文件扩展名

¥New File Extensions

package.json 中的 type 字段很棒,因为它允许我们继续使用 .ts.js 文件扩展名,这很方便;但是,你偶尔需要编写与 type 指定的文件不同的文件。你可能也只是希望始终保持显式。

¥The type field in package.json is nice because it allows us to continue using the .ts and .js file extensions which can be convenient; however, you will occasionally need to write a file that differs from what type specifies. You might also just prefer to always be explicit.

Node.js 支持两个扩展来帮助实现这一点:.mjs.cjs.mjs 文件始终是 ES 模块,而 .cjs 文件始终是 CommonJS 模块,并且无法覆盖它们。

¥Node.js supports two extensions to help with this: .mjs and .cjs. .mjs files are always ES modules, and .cjs files are always CommonJS modules, and there’s no way to override these.

TypeScript 支持两种新的源文件扩展名:.mts.cts。当 TypeScript 将这些文件发送到 JavaScript 文件时,它会分别发送到 .mjs.cjs

¥In turn, TypeScript supports two new source file extensions: .mts and .cts. When TypeScript emits these to JavaScript files, it will emit them to .mjs and .cjs respectively.

此外,TypeScript 还支持两种新的声明文件扩展名:.d.mts.d.cts。当 TypeScript 为 .mts.cts 生成声明文件时,它们对应的扩展名将是 .d.mts.d.cts

¥Furthermore, TypeScript also supports two new declaration file extensions: .d.mts and .d.cts. When TypeScript generates declaration files for .mts and .cts, their corresponding extensions will be .d.mts and .d.cts.

使用这些扩展完全是可选的,但即使你选择不将它们用作主要工作流程的一部分,它们通常也很有用。

¥Using these extensions is entirely optional, but will often be useful even if you choose not to use them as part of your primary workflow.

CommonJS 互操作性

¥CommonJS Interoperability

Node.js 允许 ES 模块导入 CommonJS 模块,就像它们是具有默认导出的 ES 模块一样。

¥Node.js allows ES modules to import CommonJS modules as if they were ES modules with a default export.

ts
// ./foo.cts
export function helper() {
console.log("hello world!");
}
// ./bar.mts
import foo from "./foo.cjs";
// prints "hello world!"
foo.helper();

在某些情况下,Node.js 还会从 CommonJS 模块合成命名导出,这会更加方便。在这些情况下,ES 模块可以使用 “namespace-style” 导入(即 import * as foo from "...")或命名导入(即 import { helper } from "...")。

¥In some cases, Node.js also synthesizes named exports from CommonJS modules, which can be more convenient. In these cases, ES modules can use a “namespace-style” import (i.e. import * as foo from "..."), or named imports (i.e. import { helper } from "...").

ts
// ./foo.cts
export function helper() {
console.log("hello world!");
}
// ./bar.mts
import { helper } from "./foo.cjs";
// prints "hello world!"
helper();

TypeScript 并不总是能够知道这些命名导入是否会被合成,但 TypeScript 会犯宽容的错误,并在从肯定是 CommonJS 模块的文件导入时使用一些启发式算法。

¥There isn’t always a way for TypeScript to know whether these named imports will be synthesized, but TypeScript will err on being permissive and use some heuristics when importing from a file that is definitely a CommonJS module.

关于互操作,TypeScript 特有的一个注释是以下语法:

¥One TypeScript-specific note about interop is the following syntax:

ts
import foo = require("foo");

在 CommonJS 模块中,这归结为 require() 调用,而在 ES 模块中,导入 createRequire 来实现相同的目的。这将降低代码在浏览器(不支持 require())等运行时上的可移植性,但通常对互操作性很有用。你可以使用以下语法编写上述示例:

¥In a CommonJS module, this just boils down to a require() call, and in an ES module, this imports createRequire to achieve the same thing. This will make code less portable on runtimes like the browser (which don’t support require()), but will often be useful for interoperability. In turn, you can write the above example using this syntax as follows:

ts
// ./foo.cts
export function helper() {
console.log("hello world!");
}
// ./bar.mts
import foo = require("./foo.cjs");
foo.helper()

最后,值得注意的是,从 CJS 模块导入 ESM 文件的唯一方法是使用动态 import() 调用。这可能会带来一些挑战,但这是 Node.js 目前的做法。

¥Finally, it’s worth noting that the only way to import ESM files from a CJS module is using dynamic import() calls. This can present challenges, but is the behavior in Node.js today.

你可以 在此处阅读有关 Node.js 中 ESM/CommonJS 互操作的更多信息

¥You can read more about ESM/CommonJS interop in Node.js here.

package.json 导出、导入和自引用

¥package.json Exports, Imports, and Self-Referencing

Node.js 支持 package.json 中用于定义入口点的新字段,名为 "exports"。此字段是比在 package.json 中定义 "main" 更强大的替代方案,可以控制包的哪些部分向用户公开。

¥Node.js supports a new field for defining entry points in package.json called "exports". This field is a more powerful alternative to defining "main" in package.json, and can control what parts of your package are exposed to consumers.

这是一个支持 CommonJS 和 ESM 单独入口点的 package.json

¥Here’s a package.json that supports separate entry-points for CommonJS and ESM:

jsonc
// package.json
{
"name": "my-package",
"type": "module",
"exports": {
".": {
// Entry-point for `import "my-package"` in ESM
"import": "./esm/index.js",
// Entry-point for `require("my-package") in CJS
"require": "./commonjs/index.cjs",
},
},
// CJS fall-back for older versions of Node.js
"main": "./commonjs/index.cjs",
}

你可以在 Node.js 文档中了解更多信息 这个功能有很多内容。此处,我们将重点介绍 TypeScript 如何支持它。

¥There’s a lot to this feature, which you can read more about on the Node.js documentation. Here we’ll try to focus on how TypeScript supports it.

在 TypeScript 原生 Node 支持下,它会查找 "main" 字段,然后查找与该字段对应的声明文件。例如,如果 "main" 指向 ./lib/index.js,TypeScript 会查找名为 ./lib/index.d.ts 的文件。包作者可以通过指定一个名为 "types" 的单独字段(例如 "types": "./types/index.d.ts")来覆盖此功能。

¥With TypeScript’s original Node support, it would look for a "main" field, and then look for declaration files that corresponded to that entry. For example, if "main" pointed to ./lib/index.js, TypeScript would look for a file called ./lib/index.d.ts. A package author could override this by specifying a separate field called "types" (e.g. "types": "./types/index.d.ts").

新的支持与 导入条件 类似。默认情况下,TypeScript 会将相同的规则与导入条件叠加。 - 如果你从 ES 模块编写 import,它将查找 import 字段,而从 CommonJS 模块编写 import,它将查找 require 字段。如果找到,它将查找相应的声明文件。如果你需要将类型声明指向其他位置,可以添加 "types" 导入条件。

¥The new support works similarly with import conditions. By default, TypeScript overlays the same rules with import conditions - if you write an import from an ES module, it will look up the import field, and from a CommonJS module, it will look at the require field. If it finds them, it will look for a corresponding declaration file. If you need to point to a different location for your type declarations, you can add a "types" import condition.

jsonc
// package.json
{
"name": "my-package",
"type": "module",
"exports": {
".": {
// Entry-point for `import "my-package"` in ESM
"import": {
// Where TypeScript will look.
"types": "./types/esm/index.d.ts",
// Where Node.js will look.
"default": "./esm/index.js"
},
// Entry-point for `require("my-package") in CJS
"require": {
// Where TypeScript will look.
"types": "./types/commonjs/index.d.cts",
// Where Node.js will look.
"default": "./commonjs/index.cjs"
},
}
},
// Fall-back for older versions of TypeScript
"types": "./types/index.d.ts",
// CJS fall-back for older versions of Node.js
"main": "./commonjs/index.cjs"
}

"types" 条件在 "exports" 中应始终放在首位。

¥The "types" condition should always come first in "exports".

需要注意的是,CommonJS 入口点和 ES 模块入口点各自都需要各自的声明文件,即使它们之间的内容相同。每个声明文件都会根据其文件扩展名和 package.json 中的 "type" 字段,被解释为 CommonJS 模块或 ES 模块。检测到的模块类型必须与 Node 为相应 JavaScript 文件检测的模块类型匹配,以确保类型检查正确。尝试使用单个 .d.ts 文件同时为 ES 模块入口点和 CommonJS 入口点设置类型会导致 TypeScript 认为只存在其中一个入口点,从而导致包用户出现编译器错误。

¥It’s important to note that the CommonJS entrypoint and the ES module entrypoint each needs its own declaration file, even if the contents are the same between them. Every declaration file is interpreted either as a CommonJS module or as an ES module, based on its file extension and the "type" field of the package.json, and this detected module kind must match the module kind that Node will detect for the corresponding JavaScript file for type checking to be correct. Attempting to use a single .d.ts file to type both an ES module entrypoint and a CommonJS entrypoint will cause TypeScript to think only one of those entrypoints exists, causing compiler errors for users of the package.

TypeScript 也以类似的方式支持 package.json"imports" 字段,即在声明文件旁边查找相应的文件,并支持 软件包自引用。这些功能通常不需要设置,但仍然受支持。

¥TypeScript also supports the "imports" field of package.json in a similar manner by looking for declaration files alongside corresponding files, and supports packages self-referencing themselves. These features are generally not as involved to set up, but are supported.

期待你的反馈!

¥Your Feedback Wanted!

随着我们继续开发 TypeScript 4.7,我们期待看到更多关于此功能的文档和完善。支持这些新功能是一项雄心勃勃的任务,因此我们期待尽早收到反馈!请尝试一下,并告诉我们它对你有何作用。

¥As we continue working on TypeScript 4.7, we expect to see more documentation and polish go into this functionality. Supporting these new features has been an ambitious under-taking, and that’s why we’re looking for early feedback on it! Please try it out and let us know how it works for you.

更多信息请见 你可以在此处查看实现 PR

¥For more information, you can see the implementing PR here.

模块检测控制

¥Control over Module Detection

将模块引入 JavaScript 的一个问题是现有 “script” 代码和新模块代码之间的歧义。模块中的 JavaScript 代码运行方式略有不同,并且具有不同的作用域规则,因此工具必须确定每个文件的运行方式。例如,Node.js 要求模块入口点必须使用 .mjs 类型,或者 package.json"type": "module" 类型相邻。TypeScript 会在文件中找到任何 importexport 语句时,将其视为模块;否则,会将 .ts.js 文件视为作用于全局作用域的脚本文件。

¥One issue with the introduction of modules to JavaScript was the ambiguity between existing “script” code and the new module code. JavaScript code in a module runs slightly differently, and has different scoping rules, so tools have to make decisions as to how each file runs. For example, Node.js requires module entry-points to be written in a .mjs, or have a nearby package.json with "type": "module". TypeScript treats a file as a module whenever it finds any import or export statement in a file, but otherwise, will assume a .ts or .js file is a script file acting on the global scope.

这与 Node.js 的行为不太匹配,在 Node.js 中,package.json 可以更改文件的格式,而 --jsx 设置 react-jsx 时,任何 JSX 文件都包含对 JSX 工厂的隐式导入。它也不符合现代人的期望,因为大多数新的 TypeScript 代码都是以模块为导向编写的。

¥This doesn’t quite match up with the behavior of Node.js where the package.json can change the format of a file, or the --jsx setting react-jsx, where any JSX file contains an implicit import to a JSX factory. It also doesn’t match modern expectations where most new TypeScript code is written with modules in mind.

这就是为什么 TypeScript 4.7 引入了一个名为 moduleDetection 的新选项。moduleDetection 可以采用 3 个值:"auto"(默认)、"legacy"(与 4.6 及之前版本的行为相同)和 "force"

¥That’s why TypeScript 4.7 introduces a new option called moduleDetection. moduleDetection can take on 3 values: "auto" (the default), "legacy" (the same behavior as 4.6 and prior), and "force".

"auto" 模式下,TypeScript 不仅会查找 importexport 语句,还会检查

¥Under the mode "auto", TypeScript will not only look for import and export statements, but it will also check whether

  • --module nodenext/--module node16 下运行时,package.json 中的 "type" 字段设置为 "module",并且

    ¥the "type" field in package.json is set to "module" when running under --module nodenext/--module node16, and

  • --jsx react-jsx 模式下运行时,检查当前文件是否为 JSX 文件

    ¥check whether the current file is a JSX file when running under --jsx react-jsx

如果你希望将每个文件都视为一个模块,"force" 设置可确保每个未声明的文件都被视为一个模块。无论 modulemoduleResolutionjsx 如何配置,这都是正确的。

¥In cases where you want every file to be treated as a module, the "force" setting ensures that every non-declaration file is treated as a module. This will be true regardless of how module, moduleResolution, and jsx are configured.

同时,"legacy" 选项只是恢复了旧的行为,即只查找 importexport 语句来确定文件是否为模块。

¥Meanwhile, the "legacy" option simply goes back to the old behavior of only seeking out import and export statements to determine whether a file is a module.

你可以 在拉取请求中了解更多关于此变更的信息

¥You can read up more about this change on the pull request.

括号元素访问的控制流分析

¥Control-Flow Analysis for Bracketed Element Access

当索引键是字面量类型和唯一符号时,TypeScript 4.7 现在会缩小元素访问的类型。例如,采用以下代码:

¥TypeScript 4.7 now narrows the types of element accesses when the indexed keys are literal types and unique symbols. For example, take the following code:

ts
const key = Symbol();
const numberOrString = Math.random() < 0.5 ? 42 : "hello";
const obj = {
[key]: numberOrString,
};
if (typeof obj[key] === "string") {
let str = obj[key].toUpperCase();
}

以前,TypeScript 不会考虑 obj[key] 上的任何类型保护,也不知道 obj[key] 实际上是 string。相反,它会认为 obj[key] 仍然是 string | number,访问 toUpperCase() 会触发错误。

¥Previously, TypeScript would not consider any type guards on obj[key], and would have no idea that obj[key] was really a string. Instead, it would think that obj[key] was still a string | number and accessing toUpperCase() would trigger an error.

TypeScript 4.7 现在知道 obj[key] 是一个字符串。

¥TypeScript 4.7 now knows that obj[key] is a string.

这也意味着在 --strictPropertyInitialization 下,TypeScript 可以正确检查计算属性是否在构造函数体末尾初始化。

¥This also means that under --strictPropertyInitialization, TypeScript can correctly check that computed properties are initialized by the end of a constructor body.

ts
// 'key' has type 'unique symbol'
const key = Symbol();
class C {
[key]: string;
constructor(str: string) {
// oops, forgot to set 'this[key]'
}
screamString() {
return this[key].toUpperCase();
}
}

在 TypeScript 4.7 下,--strictPropertyInitialization 会报告一个错误,告诉我们 [key] 属性在构造函数结束时未被明确赋值。

¥Under TypeScript 4.7, --strictPropertyInitialization reports an error telling us that the [key] property wasn’t definitely assigned by the end of the constructor.

我们要感谢 Oleksandr Tarasiuk 提供的 此项变更

¥We’d like to extend our gratitude to Oleksandr Tarasiuk who provided this change!

改进了对象和方法中的函数推断

¥Improved Function Inference in Objects and Methods

TypeScript 4.7 现在可以从对象和数组中的函数执行更细粒度的推断。这使得这些函数的类型能够像普通参数一样以从左到右的方式一致流动。

¥TypeScript 4.7 can now perform more granular inferences from functions within objects and arrays. This allows the types of these functions to consistently flow in a left-to-right manner just like for plain arguments.

ts
declare function f<T>(arg: {
produce: (n: string) => T,
consume: (x: T) => void }
): void;
// Works
f({
produce: () => "hello",
consume: x => x.toLowerCase()
});
// Works
f({
produce: (n: string) => n,
consume: x => x.toLowerCase(),
});
// Was an error, now works.
f({
produce: n => n,
consume: x => x.toLowerCase(),
});
// Was an error, now works.
f({
produce: function () { return "hello"; },
consume: x => x.toLowerCase(),
});
// Was an error, now works.
f({
produce() { return "hello" },
consume: x => x.toLowerCase(),
});

在其中一些示例中,推断失败了,因为在找到 T 的合适类型之前,知道 produce 函数的类型会间接请求 arg 的类型。TypeScript 现在会收集可能有助于推断 T 类型的函数,并对其进行惰性推断。

¥Inference failed in some of these examples because knowing the type of their produce functions would indirectly request the type of arg before finding a good type for T. TypeScript now gathers functions that could contribute to the inferred type of T and infers from them lazily.

更多信息,你可以查看 查看我们推断过程的具体修改

¥For more information, you can take a look at the specific modifications to our inference process.

实例化表达式

¥Instantiation Expressions

有时,函数可能比我们想要的更通用。例如,假设我们有一个 makeBox 函数。

¥Occasionally functions can be a bit more general than we want. For example, let’s say we had a makeBox function.

ts
interface Box<T> {
value: T;
}
function makeBox<T>(value: T) {
return { value };
}

也许我们想创建一组更专业的函数,用于将 WrenchHammer 转换为 Box。现在,为了做到这一点,我们必须将 makeBox 封装在其他函数中,或者使用显式类型作为 makeBox 的别名。

¥Maybe we want to create a more specialized set of functions for making Boxes of Wrenches and Hammers. To do that today, we’d have to wrap makeBox in other functions, or use an explicit type for an alias of makeBox.

ts
function makeHammerBox(hammer: Hammer) {
return makeBox(hammer);
}
// or...
const makeWrenchBox: (wrench: Wrench) => Box<Wrench> = makeBox;

这些方法可以工作,但封装对 makeBox 的调用有点浪费,并且编写 makeWrenchBox 的完整签名可能会变得笨拙。理想情况下,我们可以说我们只想为 makeBox 添加别名,同时替换其签名中的所有泛型。

¥These work, but wrapping a call to makeBox is a bit wasteful, and writing the full signature of makeWrenchBox could get unwieldy. Ideally, we would be able to say that we just want to alias makeBox while replacing all of the generics in its signature.

TypeScript 4.7 正是如此!现在我们可以直接使用函数和构造函数作为类型参数。

¥TypeScript 4.7 allows exactly that! We can now take functions and constructors and feed them type arguments directly.

ts
const makeHammerBox = makeBox<Hammer>;
const makeWrenchBox = makeBox<Wrench>;

因此,我们可以特化 makeBox 以接受更具体的类型并拒绝任何其他类型。

¥So with this, we can specialize makeBox to accept more specific types and reject anything else.

ts
const makeStringBox = makeBox<string>;
// TypeScript correctly rejects this.
makeStringBox(42);

此逻辑也适用于构造函数,例如 ArrayMapSet

¥This logic also works for constructor functions such as Array, Map, and Set.

ts
// Has type `new () => Map<string, Error>`
const ErrorMap = Map<string, Error>;
// Has type `// Map<string, Error>`
const errorMap = new ErrorMap();

当函数或构造函数被赋予类型参数时,它将生成一个新类型,该类型保留所有签名和兼容的类型参数列表,并用给定的类型参数替换相应的类型参数。任何其他签名都将被删除,因为 TypeScript 会假定它们不打算使用。

¥When a function or constructor is given type arguments, it will produce a new type that keeps all signatures with compatible type parameter lists, and replaces the corresponding type parameters with the given type arguments. Any other signatures are dropped, as TypeScript will assume that they aren’t meant to be used.

有关此功能的更多信息,请参阅 查看拉取请求

¥For more information on this feature, check out the pull request.

extends infer 类型变量的约束

¥extends Constraints on infer Type Variables

条件类型是一项高级用户功能。它们允许我们匹配和推断类型的形状,并基于它们做出决策。例如,我们可以编写一个条件类型,如果元组类型是类似 string 的类型,则返回其第一个元素。

¥Conditional types are a bit of a power-user feature. They allow us to match and infer against the shape of types, and make decisions based on them. For example, we can write a conditional type that returns the first element of a tuple type if it’s a string-like type.

ts
type FirstIfString<T> =
T extends [infer S, ...unknown[]]
? S extends string ? S : never
: never;
// string
type A = FirstIfString<[string, number, number]>;
// "hello"
type B = FirstIfString<["hello", number, number]>;
// "hello" | "world"
type C = FirstIfString<["hello" | "world", boolean]>;
// never
type D = FirstIfString<[boolean, number, string]>;

FirstIfString 匹配任何至少包含一个元素的元组,并将第一个元素的类型获取为 S。然后它会检查 S 是否与 string 兼容,如果兼容,则返回该类型。

¥FirstIfString matches against any tuple with at least one element and grabs the type of the first element as S. Then it checks if S is compatible with string and returns that type if it is.

请注意,我们必须使用两种条件类型来编写此代码。我们可以将 FirstIfString 写成如下形式:

¥Note that we had to use two conditional types to write this. We could have written FirstIfString as follows:

ts
type FirstIfString<T> =
T extends [string, ...unknown[]]
// Grab the first type out of `T`
? T[0]
: never;

这可行,但它更像 “manual”,声明性更低。我们不再仅仅对类型进行模式匹配并为第一个元素命名,而是必须使用 T[0] 取出 T 的第 0 个元素。如果我们处理的类型比元组更复杂,这可能会变得更加棘手,因此 infer 可以简化事情。

¥This works, but it’s slightly more “manual” and less declarative. Instead of just pattern-matching on the type and giving the first element a name, we have to fetch out the 0th element of T with T[0]. If we were dealing with types more complex than tuples, this could get a lot trickier, so infer can simplify things.

使用嵌套条件推断类型,然后与推断出的类型进行匹配是很常见的。为了避免第二层嵌套,TypeScript 4.7 现在允许你对任何 infer 类型添加约束。

¥Using nested conditionals to infer a type and then match against that inferred type is pretty common. To avoid that second level of nesting, TypeScript 4.7 now allows you to place a constraint on any infer type.

ts
type FirstIfString<T> =
T extends [infer S extends string, ...unknown[]]
? S
: never;

这样,当 TypeScript 与 S 匹配时,它还会确保 S 必须是 string。如果 S 不是 string,则会采用错误路径,在这些情况下为 never

¥This way, when TypeScript matches against S, it also ensures that S has to be a string. If S isn’t a string, it takes the false path, which in these cases is never.

更多详情,请参阅 在 GitHub 上阅读变更信息

¥For more details, you can read up on the change on GitHub.

类型参数的可选变体注解

¥Optional Variance Annotations for Type Parameters

让我们采用以下类型。

¥Let’s take the following types.

ts
interface Animal {
animalStuff: any;
}
interface Dog extends Animal {
dogStuff: any;
}
// ...
type Getter<T> = () => T;
type Setter<T> = (value: T) => void;

假设我们有两个不同的 Getter 实例。判断两个不同的 Getter 是否可以相互替代完全取决于 T。为了判断 Getter<Dog>Getter<Animal> 的赋值是否有效,我们必须检查 DogAnimal 是否有效。由于 T 的每种类型在同一个 “direction” 中都相互关联,因此我们称 Getter 类型在 T 上是协变的。另一方面,检查 Setter<Dog>Setter<Animal> 是否有效涉及检查 AnimalDog 是否有效。“flip” 的方向有点像数学中检查 −x < −y 是否与检查 y < x 是否相同。当我们必须像这样翻转方向来比较 T 时,我们说 SetterT 是逆变的。

¥Imagine we had two different instances of Getters. Figuring out whether any two different Getters are substitutable for one another depends entirely on T. In the case of whether an assignment of Getter<Dog> → Getter<Animal> is valid, we have to check whether Dog → Animal is valid. Because each type for T just gets related in the same “direction”, we say that the Getter type is covariant on T. On the other hand, checking whether Setter<Dog> → Setter<Animal> is valid involves checking whether Animal → Dog is valid. That “flip” in direction is kind of like how in math, checking whether −x < −y is the same as checking whether y < x. When we have to flip directions like this to compare T, we say that Setter is contravariant on T.

在 TypeScript 4.7 中,我们现在可以明确指定类型参数的变体。

¥With TypeScript 4.7, we’re now able to explicitly specify variance on type parameters.

所以现在,如果我们想明确地表明 GetterT 是协变的,我们可以给它一个 out 修饰符。

¥So now, if we want to make it explicit that Getter is covariant on T, we can now give it an out modifier.

ts
type Getter<out T> = () => T;

类似地,如果我们也想明确 SetterT 是逆变的,我们可以给它一个 in 修饰符。

¥And similarly, if we also want to make it explicit that Setter is contravariant on T, we can give it an in modifier.

ts
type Setter<in T> = (value: T) => void;

这里使用 outin 是因为类型参数的方差取决于它是用于输出还是输入。你无需考虑方差,只需考虑 T 是否用于输出和输入位置即可。

¥out and in are used here because a type parameter’s variance depends on whether it’s used in an output or an input. Instead of thinking about variance, you can just think about if T is used in output and input positions.

也存在同时使用 inout 的情况。

¥There are also cases for using both in and out.

ts
interface State<in out T> {
get: () => T;
set: (value: T) => void;
}

T 同时用于输出和输入位置时,它将变为不变。除非两个不同的 State<T> 相同,否则它们的 T 不能互换。换句话说,State<Dog>State<Animal> 不能互相替代。

¥When a T is used in both an output and input position, it becomes invariant. Two different State<T>s can’t be interchanged unless their Ts are the same. In other words, State<Dog> and State<Animal> aren’t substitutable for the other.

从技术上讲,在纯结构化类型系统中,类型参数及其变体实际上并不重要。 - 你只需在每个类型参数的位置插入类型,并检查每个匹配成员是否在结构上兼容即可。那么,如果 TypeScript 使用结构化类型系统,我们为什么还要关注类型参数的方差呢?我们为什么要注释它们呢?

¥Now technically speaking, in a purely structural type system, type parameters and their variance don’t really matter - you can just plug in types in place of each type parameter and check whether each matching member is structurally compatible. So if TypeScript uses a structural type system, why are we interested in the variance of type parameters? And why might we ever want to annotate them?

原因之一是,它可以让读者一目了然地了解类型参数的使用方式。对于更复杂的类型,很难判断类型是应该读取、写入还是两者兼而有之。如果我们忘记提及该类型参数的使用方式,TypeScript 也会提供帮助。例如,如果我们忘记在 State 上同时指定 inout,就会出现错误。

¥One reason is that it can be useful for a reader to explicitly see how a type parameter is used at a glance. For much more complex types, it can be difficult to tell whether a type is meant to be read, written, or both. TypeScript will also help us out if we forget to mention how that type parameter is used. As an example, if we forgot to specify both in and out on State, we’d get an error.

ts
interface State<out T> {
// ~~~~~
// error!
// Type 'State<sub-T>' is not assignable to type 'State<super-T>' as implied by variance annotation.
// Types of property 'set' are incompatible.
// Type '(value: sub-T) => void' is not assignable to type '(value: super-T) => void'.
// Types of parameters 'value' and 'value' are incompatible.
// Type 'super-T' is not assignable to type 'sub-T'.
get: () => T;
set: (value: T) => void;
}

另一个原因是精度和速度!作为一种优化,TypeScript 已经尝试推断类型参数的方差。这样一来,它可以在合理的时间内对较大的结构类型进行类型检查。提前计算变体可以让类型检查器跳过更深层次的比较,只比较类型参数,这比反复比较类型的完整结构要快得多。但通常情况下,这种计算仍然相当昂贵,并且计算可能会发现无法准确解析的循环,这意味着对于类型的方差没有明确的答案。

¥Another reason is precision and speed! TypeScript already tries to infer the variance of type parameters as an optimization. By doing this, it can type-check larger structural types in a reasonable amount of time. Calculating variance ahead of time allows the type-checker to skip deeper comparisons and just compare type arguments which can be much faster than comparing the full structure of a type over and over again. But often there are cases where this calculation is still fairly expensive, and the calculation may find circularities that can’t be accurately resolved, meaning there’s no clear answer for the variance of a type.

ts
type Foo<T> = {
x: T;
f: Bar<T>;
}
type Bar<U> = (x: Baz<U[]>) => void;
type Baz<V> = {
value: Foo<V[]>;
}
declare let foo1: Foo<unknown>;
declare let foo2: Foo<string>;
foo1 = foo2; // Should be an error but isn't ❌
foo2 = foo1; // Error - correct ✅

提供显式注解可以加快这些循环的类型检查,并提高准确性。例如,在上面的例子中,将 T 标记为不变式可以帮助停止有问题的赋值。

¥Providing an explicit annotation can speed up type-checking at these circularities and provide better accuracy. For instance, marking T as invariant in the above example can help stop the problematic assignment.

diff
- type Foo<T> = {
+ type Foo<in out T> = {
x: T;
f: Bar<T>;
}

我们不建议为每个类型参数都添加其变体注释;例如,可以(但不建议)将方差限制得比必要的更严格一些,因此,如果某些内容实际上只是协变、逆变甚至是独立的,TypeScript 不会阻止你将它标记为不变。所以,如果你确实选择添加显式方差标记,我们鼓励你谨慎且准确地使用它们。

¥We don’t necessarily recommend annotating every type parameter with its variance; For example, it’s possible (but not recommended) to make variance a little stricter than is necessary, so TypeScript won’t stop you from marking something as invariant if it’s really just covariant, contravariant, or even independent. So if you do choose to add explicit variance markers, we would encourage thoughtful and precise use of them.

如果你正在使用深度递归类型,尤其是如果你是库作者,你可能有兴趣使用这些注解来造福你的用户。这些注解在准确性和类型检查速度方面都有所提升,甚至会影响代码编辑体验。可以通过实验确定方差计算何时会成为类型检查时间的瓶颈,并使用类似我们的 analyze-trace 工具的工具进行确定。

¥But if you’re working with deeply recursive types, especially if you’re a library author, you may be interested in using these annotations to the benefit of your users. Those annotations can provide wins in both accuracy and type-checking speed, which can even affect their code editing experience. Determining when variance calculation is a bottleneck on type-checking time can be done experimentally, and determined using tooling like our analyze-trace utility.

更多此功能详情,请访问 阅读拉取请求信息

¥For more details on this feature, you can read up on the pull request.

使用 moduleSuffixes 进行解析自定义

¥Resolution Customization with moduleSuffixes

TypeScript 4.7 现在支持 moduleSuffixes 选项,用于自定义模块说明符的查找方式。

¥TypeScript 4.7 now supports a moduleSuffixes option to customize how module specifiers are looked up.

jsonc
{
"compilerOptions": {
"moduleSuffixes": [".ios", ".native", ""]
}
}

给定上述配置,导入如下……

¥Given the above configuration, an import like the following…

ts
import * as foo from "./foo";

将尝试查看相关文件 ./foo.ios.ts./foo.native.ts,最后是 ./foo.ts

¥will try to look at the relative files ./foo.ios.ts, ./foo.native.ts, and finally ./foo.ts.

此功能对于 React Native 项目非常有用,因为每个目标平台都可以使用单独的 tsconfig.json 和不同的 moduleSuffixes

¥This feature can be useful for React Native projects where each target platform can use a separate tsconfig.json with differing moduleSuffixes.

moduleSuffixes 选项 的贡献要感谢 Adam Foxman

¥The moduleSuffixes option was contributed thanks to Adam Foxman!

resolution-mode

使用 Node 的 ECMAScript 解析,包含文件的模式和你使用的语法决定了导入的解析方式;但是,从 ECMAScript 模块引用 CommonJS 模块的类型会很有用,反之亦然。

¥With Node’s ECMAScript resolution, the mode of the containing file and the syntax you use determines how imports are resolved; however it would be useful to reference the types of a CommonJS module from an ECMAScript module, or vice-versa.

TypeScript 现在允许使用 /// <reference types="..." /> 指令。

¥TypeScript now allows /// <reference types="..." /> directives.

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

此外,在 TypeScript 的夜间版本中,import type 可以指定导入断言来实现类似的功能。

¥Additionally, in nightly versions of TypeScript, import type can specify an import assertion to achieve something similar.

ts
// Resolve `pkg` as if we were importing with a `require()`
import type { TypeFromRequire } from "pkg" assert {
"resolution-mode": "require"
};
// Resolve `pkg` as if we were importing with an `import`
import type { TypeFromImport } from "pkg" assert {
"resolution-mode": "import"
};
export interface MergedType extends TypeFromRequire, TypeFromImport {}

这些导入断言也可用于 import() 类型。

¥These import assertions can also be used on import() types.

ts
export type TypeFromRequire =
import("pkg", { assert: { "resolution-mode": "require" } }).TypeFromRequire;
export type TypeFromImport =
import("pkg", { assert: { "resolution-mode": "import" } }).TypeFromImport;
export interface MergedType extends TypeFromRequire, TypeFromImport {}

import typeimport() 语法仅支持 TypeScript 的每日构建版本 中的 resolution-mode。你可能会收到类似以下错误:

¥The import type and import() syntaxes only support resolution-mode in nightly builds of TypeScript. You’ll likely get an error like

Resolution mode assertions are unstable. Use nightly TypeScript to silence this error. Try updating with 'npm install -D typescript@next'.

如果你确实在 TypeScript 的 Nightly 版本中使用此功能,请使用 考虑就此问题提供反馈

¥If you do find yourself using this feature in nightly versions of TypeScript, consider providing feedback on this issue.

你可以查看 用于参考指令用于类型导入断言 的相应更改。

¥You can see the respective changes for reference directives and for type import assertions.

跳转到源定义

¥Go to Source Definition

TypeScript 4.7 支持一个名为“转到源定义”的新实验性编辑器命令。它类似于“转到定义”,但它永远不会在声明文件中返回结果。相反,它会尝试查找相应的实现文件(例如 .js.ts 文件),并在其中查找定义 - 即使这些文件通常被 .d.ts 文件遮蔽。

¥TypeScript 4.7 contains support for a new experimental editor command called Go To Source Definition. It’s similar to Go To Definition, but it never returns results inside declaration files. Instead, it tries to find corresponding implementation files (like .js or .ts files), and find definitions there — even if those files are normally shadowed by .d.ts files.

当你需要查看从库中导入的函数的实现,而不是查看 .d.ts 文件中的类型声明时,这通常非常有用。

¥This comes in handy most often when you need to peek at the implementation of a function you’re importing from a library instead of its type declaration in a .d.ts file.

The "Go to Source Definition" command on a use of the yargs package jumps the editor to an index.cjs file in yargs.

你可以在最新版本的 Visual Studio Code 中尝试这个新命令。但请注意,此功能仍处于预览阶段,并且存在一些已知的限制。在某些情况下,TypeScript 会使用启发式方法来猜测哪个 .js 文件对应于定义的给定结果,因此这些结果可能不准确。Visual Studio Code 尚未表明结果是否为猜测,但我们正在合作解决。

¥You can try this new command in the latest versions of Visual Studio Code. Note, though, that this functionality is still in preview, and there are some known limitations. In some cases TypeScript uses heuristics to guess which .js file corresponds to the given result of a definition, so these results might be inaccurate. Visual Studio Code also doesn’t yet indicate whether a result was a guess, but it’s something we’re collaborating on.

你可以留下关于该功能的反馈,阅读已知限制,或在 我们专门的反馈问题 上了解更多信息。

¥You can leave feedback about the feature, read about known limitations, or learn more at our dedicated feedback issue.

基于组感知的组织导入

¥Group-Aware Organize Imports

TypeScript 为 JavaScript 和 TypeScript 都提供了一个 Organize Imports 编辑器功能。遗憾的是,它可能有点生硬,并且经常会天真地对导入语句进行排序。

¥TypeScript has an Organize Imports editor feature for both JavaScript and TypeScript. Unfortunately, it could be a bit of a blunt instrument, and would often naively sort your import statements.

例如,如果你在以下文件上运行了 Organize Imports……

¥For instance, if you ran Organize Imports on the following file…

ts
// local code
import * as bbb from "./bbb";
import * as ccc from "./ccc";
import * as aaa from "./aaa";
// built-ins
import * as path from "path";
import * as child_process from "child_process"
import * as fs from "fs";
// some code...

你将得到类似以下内容的结果

¥You would get something like the following

ts
// local code
import * as child_process from "child_process";
import * as fs from "fs";
// built-ins
import * as path from "path";
import * as aaa from "./aaa";
import * as bbb from "./bbb";
import * as ccc from "./ccc";
// some code...

这是……并不理想。当然,我们的导入会按路径排序,注释和换行符也会保留,但方式并非我们预期。很多时候,如果我们的导入以特定方式分组,那么我们希望保持这种分组方式。

¥This is… not ideal. Sure, our imports are sorted by their paths, and our comments and newlines are preserved, but not in a way we expected. Much of the time, if we have our imports grouped in a specific way, then we want to keep them that way.

TypeScript 4.7 以组感知的方式执行组织导入。运行上面的代码看起来更符合你的预期:

¥TypeScript 4.7 performs Organize Imports in a group-aware manner. Running it on the above code looks a little bit more like what you’d expect:

ts
// local code
import * as aaa from "./aaa";
import * as bbb from "./bbb";
import * as ccc from "./ccc";
// built-ins
import * as child_process from "child_process";
import * as fs from "fs";
import * as path from "path";
// some code...

我们要感谢 Minh Quy 贡献了 此功能

¥We’d like to extend our thanks to Minh Quy who provided this feature.

对象方法代码片段补全

¥Object Method Snippet Completions

TypeScript 现在为对象字面量方法提供代码片段补全功能。当补全对象中的成员时,TypeScript 将提供一个仅包含方法名称的典型补全条目,以及一个包含完整方法定义的单独补全条目!

¥TypeScript now provides snippet completions for object literal methods. When completing members in an object, TypeScript will provide a typical completion entry for just the name of a method, along with a separate completion entry for the full method definition!

Completion a full method signature from an object

详情请见 查看实现拉取请求

¥For more details, see the implementing pull request.

打破变更

¥Breaking Changes

lib.d.ts 更新

¥lib.d.ts Updates

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

¥While TypeScript strives to avoid major breaks, even small changes in the built-in libraries can cause issues. We don’t expect major breaks as a result of DOM and lib.d.ts updates, but there may be some small ones.

更严格的展开检查 JSX

¥Stricter Spread Checks in JSX

在 JSX 中编写 ...spread 时,TypeScript 现在会强制执行更严格的检查,以确保给定类型实际上是一个对象。因此,类型为 unknownnever(以及更罕见的,只有 nullundefined)的值将无法再扩展到 JSX 元素中。

¥When writing a ...spread in JSX, TypeScript now enforces stricter checks that the given type is actually an object. As a result, values with the types unknown and never (and more rarely, just bare null and undefined) can no longer be spread into JSX elements.

对于以下示例:

¥So for the following example:

tsx
import * as React from "react";
interface Props {
stuff?: string;
}
function MyComponent(props: unknown) {
return <div {...props} />;
}

你现在会收到如下错误:

¥you’ll now receive an error like the following:

Spread types may only be created from object types.

这使得此行为与对象字面量中的展开更加一致。

¥This makes this behavior more consistent with spreads in object literals.

详情请见 查看 GitHub 上的变更

¥For more details, see the change on GitHub.

模板字符串表达式的更严格检查

¥Stricter Checks with Template String Expressions

symbol 值用于模板字符串时,它将在 JavaScript 中触发运行时错误。

¥When a symbol value is used in a template string, it will trigger a runtime error in JavaScript.

js
let str = `hello ${Symbol()}`;
// TypeError: Cannot convert a Symbol value to a string

因此,TypeScript 也会报错;但是,TypeScript 现在还会检查模板字符串中是否使用了以某种方式限制为符号的泛型值。

¥As a result, TypeScript will issue an error as well; however, TypeScript now also checks if a generic value that is constrained to a symbol in some way is used in a template string.

ts
function logKey<S extends string | symbol>(key: S): S {
// Now an error.
console.log(`${key} is the key`);
return key;
}
function get<T, K extends keyof T>(obj: T, key: K) {
// Now an error.
console.log(`Grabbing property '${key}'.`);
return obj[key];
}

TypeScript 现在将触发以下错误:

¥TypeScript will now issue the following error:

Implicit conversion of a 'symbol' to a 'string' will fail at runtime. Consider wrapping this expression in 'String(...)'.

在某些情况下,你可以通过将表达式封装在对 String 的调用中来解决这个问题,就像错误消息所建议的那样。

¥In some cases, you can get around this by wrapping the expression in a call to String, just like the error message suggests.

ts
function logKey<S extends string | symbol>(key: S): S {
// No longer an error.
console.log(`${String(key)} is the key`);
return key;
}

在其他情况下,这个错误太过迂腐,你可能根本不会在使用 keyof 时允许使用 symbol 键。在这种情况下,你可以切换到 string & keyof ...

¥In others, this error is too pedantic, and you might not ever care to even allow symbol keys when using keyof. In such cases, you can switch to string & keyof ...:

ts
function get<T, K extends string & keyof T>(obj: T, key: K) {
// No longer an error.
console.log(`Grabbing property '${key}'.`);
return obj[key];
}

更多信息,你可以查看 查看实现拉取请求

¥For more information, you can see the implementing pull request.

readFile 方法在 LanguageServiceHost 上不再是可选的

¥readFile Method is No Longer Optional on LanguageServiceHost

如果你正在创建 LanguageService 实例,那么前提是 LanguageServiceHost 需要提供 readFile 方法。此更改对于支持新的 moduleDetection 编译器选项是必要的。

¥If you’re creating LanguageService instances, then provided LanguageServiceHosts will need to provide a readFile method. This change was necessary to support the new moduleDetection compiler option.

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

¥You can read more on the change here.

readonly 元组具有 readonly length 属性

¥readonly Tuples Have a readonly length Property

readonly 元组现在将其 length 属性视为 readonly。对于固定长度的元组来说,这几乎从未被观察到,但对于带有尾随可选和剩余元素类型的元组,可以观察到一个疏忽。

¥A readonly tuple will now treat its length property as readonly. This was almost never witnessable for fixed-length tuples, but was an oversight which could be observed for tuples with trailing optional and rest element types.

因此,以下代码现在将失败:

¥As a result, the following code will now fail:

ts
function overwriteLength(tuple: readonly [string, string, string]) {
// Now errors.
tuple.length = 7;
}

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

¥You can read more on this change here.