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
设置添加了此功能:node16
和 nodenext
。
¥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 支持名为 type
的 package.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 ofimport "./foo"
). -
导入的解析方式可能与
node_modules
中的依赖不同。¥Imports might resolve differently from dependencies in
node_modules
. -
某些类似全局的值,例如
require
和module
,不能直接使用。¥Certain global-like values like
require
andmodule
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.tsexport function helper() {// ...}// ./bar.tsimport { helper } from "./foo"; // only works in CJShelper();
此代码在 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.tsimport { helper } from "./foo.js"; // works in ESM & CJShelper();
乍一看,这可能感觉有点麻烦,但 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.ctsexport function helper() {console.log("hello world!");}// ./bar.mtsimport 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.ctsexport function helper() {console.log("hello world!");}// ./bar.mtsimport { 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.ctsexport function helper() {console.log("hello world!");}// ./bar.mtsimport 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 会在文件中找到任何 import
或 export
语句时,将其视为模块;否则,会将 .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 不仅会查找 import
和 export
语句,还会检查
¥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 inpackage.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"
设置可确保每个未声明的文件都被视为一个模块。无论 module
、moduleResolution
和 jsx
如何配置,这都是正确的。
¥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"
选项只是恢复了旧的行为,即只查找 import
和 export
语句来确定文件是否为模块。
¥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;// Worksf({produce: () => "hello",consume: x => x.toLowerCase()});// Worksf({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 };}
也许我们想创建一组更专业的函数,用于将 Wrench
和 Hammer
转换为 Box
。现在,为了做到这一点,我们必须将 makeBox
封装在其他函数中,或者使用显式类型作为 makeBox
的别名。
¥Maybe we want to create a more specialized set of functions for making Box
es of Wrench
es and Hammer
s.
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);
此逻辑也适用于构造函数,例如 Array
、Map
和 Set
。
¥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;// stringtype A = FirstIfString<[string, number, number]>;// "hello"type B = FirstIfString<["hello", number, number]>;// "hello" | "world"type C = FirstIfString<["hello" | "world", boolean]>;// nevertype 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 0
th 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>
的赋值是否有效,我们必须检查 Dog
→ Animal
是否有效。由于 T
的每种类型在同一个 “direction” 中都相互关联,因此我们称 Getter
类型在 T
上是协变的。另一方面,检查 Setter<Dog>
→ Setter<Animal>
是否有效涉及检查 Animal
→ Dog
是否有效。“flip” 的方向有点像数学中检查 −x < −y 是否与检查 y < x 是否相同。当我们必须像这样翻转方向来比较 T
时,我们说 Setter
对 T
是逆变的。
¥Imagine we had two different instances of Getter
s.
Figuring out whether any two different Getter
s 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.
所以现在,如果我们想明确地表明 Getter
与 T
是协变的,我们可以给它一个 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;
类似地,如果我们也想明确 Setter
对 T
是逆变的,我们可以给它一个 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;
这里使用 out
和 in
是因为类型参数的方差取决于它是用于输出还是输入。你无需考虑方差,只需考虑 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.
也存在同时使用 in
和 out
的情况。
¥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 T
s 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
上同时指定 in
和 out
,就会出现错误。
¥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 type
和 import()
语法仅支持 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.
你可以在最新版本的 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 codeimport * as bbb from "./bbb";import * as ccc from "./ccc";import * as aaa from "./aaa";// built-insimport * 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 codeimport * as child_process from "child_process";import * as fs from "fs";// built-insimport * 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 codeimport * as aaa from "./aaa";import * as bbb from "./bbb";import * as ccc from "./ccc";// built-insimport * as child_process from "child_process";import * as fs from "fs";import * as path from "path";// some code...
¥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!
详情请见 查看实现拉取请求。
¥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 现在会强制执行更严格的检查,以确保给定类型实际上是一个对象。因此,类型为 unknown
和 never
(以及更罕见的,只有 null
和 undefined
)的值将无法再扩展到 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 LanguageServiceHost
s 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.