TypeScript 4.7
Node.js 中的 ECMAScript 模块支持
在过去几年中,Node.js 一直在努力支持 ECMAScript 模块(ESM)。 这是一个非常困难的功能,因为 Node.js 生态系统构建在另一个称为 CommonJS(CJS)的模块系统之上。 两者之间的互操作性带来了巨大的挑战,需要协调许多新功能; 然而,Node.js 中对 ESM 的支持主要是在 Node.js 12 及更高版本中实现的。 在 TypeScript 4.5 左右,我们在 Node.js 中推出了仅限夜间版的 ESM 支持,以获取用户的反馈并让库作者为更广泛的支持做好准备。
TypeScript 4.7 通过两个新的 module 设置添加了此功能:node16 和 nodenext。
{
"compilerOptions": {
"module": "node16",
}
}这些新模式带来了一些高级功能,我们将在此处探讨。
package.json 中的 type 和新扩展名
Node.js 支持 package.json 中的一个新设置 叫做 type。 "type" 可以设置为 "module" 或 "commonjs"。
{
"name": "my-package",
"type": "module",
"//": "...",
"dependencies": {
}
}此设置控制 .js 和 .d.ts 文件是被解释为 ES 模块还是 CommonJS 模块,未设置时默认为 CommonJS。 当一个文件被认为是 ES 模块时,与 CommonJS 相比会有一些不同的规则:
- 可以使用
import/export语句。 - 可以使用顶层
await - 相对导入路径需要完整的扩展名(我们必须写
import "./foo.js"而不是import "./foo")。 - 导入可能以不同于
node_modules中依赖项的方式解析。 - 某些类似全局的值(如
require和module)不能直接使用。 - CommonJS 模块在某些特殊规则下被导入。
我们稍后会回到其中一些。
为了覆盖 TypeScript 在此系统中的工作方式,.ts 和 .tsx 文件现在以相同的方式工作。 当 TypeScript 找到一个 .ts、.tsx、.js 或 .jsx 文件时,它会向上查找 package.json 以查看该文件是否为 ES 模块,并使用它来确定:
- 如何找到该文件导入的其他模块
- 以及在生成输出时如何转换该文件
当一个 .ts 文件被编译为 ES 模块时,ECMAScript import/export 语句在 .js 输出中保持不变; 当它被编译为 CommonJS 模块时,它将产生与今天在 --module commonjs 下相同的输出。
这也意味着作为 ES 模块的 .ts 文件与作为 CJS 模块的 .ts 文件之间的路径解析方式不同。 例如,假设你今天有以下代码:
// ./foo.ts
export function helper() {
// ...
}
// ./bar.ts
import { helper } from "./foo"; // 仅在 CJS 中有效
helper();这段代码在 CommonJS 模块中有效,但在 ES 模块中会失败,因为相对导入路径需要使用扩展名。 因此,它必须被重写为使用 foo.ts 输出的扩展名——所以 bar.ts 将不得不从 ./foo.js 导入。
// ./bar.ts
import { helper } from "./foo.js"; // 在 ESM 和 CJS 中均有效
helper();一开始这可能感觉有点麻烦,但 TypeScript 工具(如自动导入和路径补全)通常会自动为你完成。
另一件需要提及的事情是,这也适用于 .d.ts 文件。 当 TypeScript 在包中找到 .d.ts 文件时,它会根据包含的包进行解释。
新文件扩展名
package.json 中的 type 字段很好,因为它允许我们继续使用 .ts 和 .js 文件扩展名,这很方便; 然而,有时你需要编写一个与 type 指定的不同的文件。 或者你可能只是更喜欢始终保持显式。
Node.js 支持两种扩展名来帮助解决这个问题:.mjs 和 .cjs。 .mjs 文件始终是 ES 模块,而 .cjs 文件始终是 CommonJS 模块,并且无法覆盖这些。
反过来,TypeScript 支持两种新的源文件扩展名:.mts 和 .cts。 当 TypeScript 将这些文件输出到 JavaScript 文件时,它将分别输出为 .mjs 和 .cjs。
此外,TypeScript 还支持两种新的声明文件扩展名:.d.mts 和 .d.cts。 当 TypeScript 为 .mts 和 .cts 生成声明文件时,它们对应的扩展名将是 .d.mts 和 .d.cts。
使用这些扩展名完全是可选的,但即使你选择不在主要工作流程中使用它们,它们也常常很有用。
CommonJS 互操作性
Node.js 允许 ES 模块导入 CommonJS 模块,就好像它们是带有默认导出的 ES 模块一样。
// ./foo.cts
export function helper() {
console.log("hello world!");
}
// ./bar.mts
import foo from "./foo.cjs";
// 打印 "hello world!"
foo.helper();在某些情况下,Node.js 还会从 CommonJS 模块合成命名导出,这可能更方便。 在这些情况下,ES 模块可以使用“命名空间风格”的导入(即 import * as foo from "..."),或命名导入(即 import { helper } from "...")。
// ./foo.cts
export function helper() {
console.log("hello world!");
}
// ./bar.mts
import { helper } from "./foo.cjs";
// 打印 "hello world!"
helper();TypeScript 并不总能知道这些命名导出是否会被合成,但 TypeScript 在从明确是 CommonJS 模块的文件导入时会保持宽容并使用一些启发式方法。
关于互操作的一个 TypeScript 特定注意事项是以下语法:
import foo = require("foo");在 CommonJS 模块中,这简化为一个 require() 调用,而在 ES 模块中,这会导入 createRequire 来实现相同的目的。 这将使代码在浏览器等运行时(不支持 require())中的可移植性降低,但对于互操作性通常很有用。 反过来,你可以使用此语法将上面的示例编写如下:
// ./foo.cts
export function helper() {
console.log("hello world!");
}
// ./bar.mts
import foo = require("./foo.cjs");
foo.helper()最后,值得注意的是,从 CJS 模块导入 ESM 文件的唯一方法是使用动态 import() 调用。 这可能会带来挑战,但这是今天 Node.js 中的行为。
你可以在此处阅读更多关于 Node.js 中 ESM/CommonJS 互操作的信息。
package.json 导出、导入和自引用
Node.js 支持一个用于在 package.json 中定义入口点的新字段,称为 "exports"。 这个字段是定义 "main" 的更强大替代方案,可以控制包的哪些部分暴露给消费者。
以下是一个为 CommonJS 和 ESM 提供单独入口点的 package.json:
// package.json
{
"name": "my-package",
"type": "module",
"exports": {
".": {
// ESM 中 `import "my-package"` 的入口点
"import": "./esm/index.js",
// CJS 中 `require("my-package")` 的入口点
"require": "./commonjs/index.cjs",
},
},
// 旧版本 Node.js 的 CJS 回退
"main": "./commonjs/index.cjs",
}这个功能有很多内容,你可以在 Node.js 文档中阅读更多信息。 这里我们将重点介绍 TypeScript 如何支持它。
使用 TypeScript 原始的 Node 支持,它会查找 "main" 字段,然后查找与该入口点对应的声明文件。 例如,如果 "main" 指向 ./lib/index.js,TypeScript 会查找名为 ./lib/index.d.ts 的文件。 包作者可以通过指定一个单独的 "types" 字段来覆盖(例如 "types": "./types/index.d.ts")。
新的支持与导入条件的工作方式类似。 默认情况下,TypeScript 叠加了相同的导入条件规则——如果你从 ES 模块编写 import,它将查找 import 字段,而如果从 CommonJS 模块,它将查看 require 字段。 如果找到它们,它将查找相应的声明文件。 如果你需要为类型声明指向不同的位置,可以添加一个 "types" 导入条件。
// package.json
{
"name": "my-package",
"type": "module",
"exports": {
".": {
// ESM 中 `import "my-package"` 的入口点
"import": {
// TypeScript 将查找的位置。
"types": "./types/esm/index.d.ts",
// Node.js 将查找的位置。
"default": "./esm/index.js"
},
// CJS 中 `require("my-package")` 的入口点
"require": {
// TypeScript 将查找的位置。
"types": "./types/commonjs/index.d.cts",
// Node.js 将查找的位置。
"default": "./commonjs/index.cjs"
},
}
},
// 旧版本 TypeScript 的回退
"types": "./types/index.d.ts",
// 旧版本 Node.js 的 CJS 回退
"main": "./commonjs/index.cjs"
}
"types"条件在"exports"中应始终放在第一位。
重要的是要注意,CommonJS 入口点和 ES 模块入口点各自需要自己的声明文件,即使它们之间的内容相同。 每个声明文件根据其文件扩展名和 package.json 的 "type" 字段被解释为 CommonJS 模块或 ES 模块,并且此检测到的模块种类必须与 Node 将为相应 JavaScript 文件检测到的模块种类匹配,以使类型检查正确。 尝试使用单个 .d.ts 文件来为 ES 模块入口点和 CommonJS 入口点提供类型将导致 TypeScript 认为只有其中一个入口点存在,从而给包的消费者带来编译器错误。
TypeScript 还以类似的方式支持 package.json 的 "imports" 字段,通过查找与相应文件一起的声明文件,并支持包自引用自身。 这些功能通常设置起来不那么复杂,但也得到支持。
期待您的反馈!
在我们继续开发 TypeScript 4.7 的过程中,我们期望看到更多关于此功能的文档和完善。 支持这些新功能是一项雄心勃勃的任务,这就是为什么我们正在寻求早期反馈! 请尝试一下,并告诉我们它对你来说效果如何。
有关更多信息,你可以在此处查看实现 PR。
模块检测控制
将模块引入 JavaScript 的一个问题是现有“脚本”代码和新模块代码之间的模糊性。 模块中的 JavaScript 代码运行略有不同,并且具有不同的作用域规则,因此工具必须决定每个文件的运行方式。 例如,Node.js 要求模块入口点写在 .mjs 中,或者附近有一个带有 "type": "module" 的 package.json。 每当 TypeScript 在文件中发现任何 import 或 export 语句时,它就会将该文件视为模块,否则将假定 .ts 或 .js 文件是在全局作用域上操作的脚本文件。
这与 Node.js 的行为不太匹配,因为 package.json 可以更改文件的格式,或者 --jsx 设置 react-jsx 会使任何 JSX 文件包含对 JSX 工厂的隐式导入。 这也与现代期望不匹配,因为大多数新的 TypeScript 代码都是考虑模块而编写的。
这就是为什么 TypeScript 4.7 引入了一个名为 moduleDetection 的新选项。 moduleDetection 可以采用 3 个值:"auto"(默认)、"legacy"(与 4.6 及更早版本相同的行为)和 "force"。
在 "auto" 模式下,TypeScript 不仅会查找 import 和 export 语句,还会检查
- 在
--module nodenext/--module node16下运行时,package.json中的"type"字段是否设置为"module",以及 - 在
--jsx react-jsx下运行时,当前文件是否为 JSX 文件
如果你希望每个文件都被视为模块,"force" 设置可确保每个非声明文件都被视为模块。 无论 module、moduleResolution 和 jsx 如何配置,都将如此。
同时,"legacy" 选项只是回到旧行为,仅通过寻找 import 和 export 语句来确定文件是否为模块。
你可以在拉取请求上阅读更多关于此更改的信息。
括号元素访问的控制流分析
TypeScript 4.7 现在在索引键是字面量类型和唯一符号时收窄元素访问的类型。 例如,考虑以下代码:
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() 会触发错误。
TypeScript 4.7 现在知道 obj[key] 是一个字符串。
这也意味着在 --strictPropertyInitialization 下,TypeScript 可以正确检查计算属性是否在构造函数体结束时初始化。
// 'key' 的类型为 'unique symbol'
const key = Symbol();
class C {
[key]: string;
constructor(str: string) {
// 哎呀,忘了设置 'this[key]'
}
screamString() {
return this[key].toUpperCase();
}
}在 TypeScript 4.7 下,--strictPropertyInitialization 报告一个错误,告诉我们 [key] 属性在构造函数结束时没有被明确赋值。
我们要感谢 Oleksandr Tarasiuk 提供了此更改!
改进的对象和方法中的函数推断
TypeScript 4.7 现在可以对对象和数组中的函数执行更细粒度的推断。 这允许这些函数的类型以从左到右的方式一致地流动,就像普通参数一样。
declare function f<T>(arg: {
produce: (n: string) => T,
consume: (x: T) => void }
): void;
// 有效
f({
produce: () => "hello",
consume: x => x.toLowerCase()
});
// 有效
f({
produce: (n: string) => n,
consume: x => x.toLowerCase(),
});
// 以前是错误,现在有效。
f({
produce: n => n,
consume: x => x.toLowerCase(),
});
// 以前是错误,现在有效。
f({
produce: function () { return "hello"; },
consume: x => x.toLowerCase(),
});
// 以前是错误,现在有效。
f({
produce() { return "hello" },
consume: x => x.toLowerCase(),
});在某些示例中,推断失败是因为知道其 produce 函数的类型会间接地请求 arg 的类型,然后才能为 T 找到好的类型。 TypeScript 现在收集可能有助于推断 T 类型的函数,并延迟从它们推断。
有关更多信息,你可以查看我们对推断过程的具体修改。
实例化表达式
有时函数可能比我们想要的更通用。 例如,假设我们有一个 makeBox 函数。
interface Box<T> {
value: T;
}
function makeBox<T>(value: T) {
return { value };
}也许我们想创建一组更专门的函数来创建 Wrench 和 Hammer 的 Box。 今天要这样做,我们必须将 makeBox 包装在其他函数中,或者为 makeBox 的别名使用显式类型。
function makeHammerBox(hammer: Hammer) {
return makeBox(hammer);
}
// 或...
const makeWrenchBox: (wrench: Wrench) => Box<Wrench> = makeBox;这些方法有效,但包装对 makeBox 的调用有点浪费,并且编写 makeWrenchBox 的完整签名可能变得笨拙。 理想情况下,我们希望能够在替换其签名中所有泛型的同时,简单地给 makeBox 起一个别名。
TypeScript 4.7 正是允许这样做的! 我们现在可以获取函数和构造函数,并直接为它们提供类型参数。
const makeHammerBox = makeBox<Hammer>;
const makeWrenchBox = makeBox<Wrench>;因此,我们可以专门化 makeBox 以接受更具体的类型并拒绝其他任何类型。
const makeStringBox = makeBox<string>;
// TypeScript 正确地拒绝了这一点。
makeStringBox(42);此逻辑也适用于构造函数,如 Array、Map 和 Set。
// 具有类型 `new () => Map<string, Error>`
const ErrorMap = Map<string, Error>;
// 具有类型 `// Map<string, Error>`
const errorMap = new ErrorMap();当函数或构造函数被赋予类型参数时,它将生成一个新类型,该类型保留所有具有兼容类型参数列表的签名,并将相应的类型参数替换为给定的类型参数。 任何其他签名都将被丢弃,因为 TypeScript 会假设它们不打算被使用。
有关此功能的更多信息,请查看拉取请求。
infer 类型变量上的 extends 约束
条件类型有点像是高级用户的功能。 它们允许我们匹配和推断类型的形状,并基于它们做出决策。 例如,我们可以编写一个条件类型,如果元组类型是类似 string 的类型,则返回其第一个元素。
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 编写如下:
type FirstIfString<T> =
T extends [string, ...unknown[]]
// 从 `T` 中获取第一个类型
? T[0]
: never;这有效,但它更“手动”,声明性稍差。 我们不是仅仅在类型上进行模式匹配并为第一个元素命名,而是必须用 T[0] 取出 T 的第 0 个元素。 如果我们处理的是比元组更复杂的类型,这可能会变得更棘手,因此 infer 可以简化事情。
使用嵌套条件类型来推断类型然后匹配该推断类型是很常见的。 为了避免第二层嵌套,TypeScript 4.7 现在允许你在任何 infer 类型上放置约束。
type FirstIfString<T> =
T extends [infer S extends string, ...unknown[]]
? S
: never;这样,当 TypeScript 匹配 S 时,它也确保 S 必须是 string。 如果 S 不是 string,它会走 false 路径,在这些情况下是 never。
有关更多详细信息,你可以在 GitHub 上阅读有关更改的信息。
类型参数的可选方差注解
让我们看以下类型。
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 的每个类型都以相同的“方向”关联,我们说 Getter 类型在 T 上是协变的。 另一方面,检查 Setter<Dog> → Setter<Animal> 是否有效涉及检查 Animal → Dog 是否有效。 这种方向的“翻转”有点像数学中检查 −x < −y 与检查 y < x 相同。 当我们必须像这样翻转方向来比较 T 时,我们说 Setter 在 T 上是逆变的。
使用 TypeScript 4.7,我们现在能够显式地指定类型参数上的方差。
因此,现在如果我们想明确 Getter 在 T 上是协变的,我们可以给它一个 out 修饰符。
type Getter<out T> = () => T;类似地,如果我们还想明确 Setter 在 T 上是逆变的,我们可以给它一个 in 修饰符。
type Setter<in T> = (value: T) => void;这里使用 out 和 in 是因为类型参数的方差取决于它是用在输出还是输入位置。 与其考虑方差,不如只考虑 T 是否用在输出和输入位置。
也有同时使用 in 和 out 的情况。
interface State<in out T> {
get: () => T;
set: (value: T) => void;
}当 T 同时用在输出和输入位置时,它变得不变。 两个不同的 State<T> 不能互换,除非它们的 T 相同。 换句话说,State<Dog> 和 State<Animal> 不能相互替换。
现在从技术上讲,在纯结构类型系统中,类型参数及其方差并不真正重要——你只需将类型插入每个类型参数的位置,并检查每个匹配成员在结构上是否兼容。 那么,如果 TypeScript 使用结构类型系统,我们为什么对类型参数的方差感兴趣? 我们为什么想要注释它们?
一个原因是,对于读者来说,能够一眼看到类型参数的使用方式可能很有用。 对于更复杂的类型,可能很难判断一个类型是用于读取、写入还是两者兼有。 如果我们忘记说明该类型参数的使用方式,TypeScript 也会帮助我们。 例如,如果我们忘记在 State 上同时指定 in 和 out,我们会得到一个错误。
interface State<out T> {
// ~~~~~
// 错误!
// 类型 'State<sub-T>' 不能赋值给类型 'State<super-T>',如方差注释所暗示。
// 属性 'set' 的类型不兼容。
// 类型 '(value: sub-T) => void' 不能赋值给类型 '(value: super-T) => void'。
// 参数 'value' 和 'value' 的类型不兼容。
// 类型 'super-T' 不能赋值给类型 'sub-T'。
get: () => T;
set: (value: T) => void;
}另一个原因是精度和速度! TypeScript 已经尝试将类型参数的方差推断作为一种优化。 通过这样做,它可以在合理的时间内对较大的结构类型进行类型检查。 提前计算方差使类型检查器能够跳过更深的比较,而只需比较类型参数,这比一遍又一遍地比较类型的完整结构快得多。 但通常存在这种情况,这种计算仍然相当昂贵,并且计算可能会发现无法准确解析的循环,这意味着类型的方差没有明确的答案。
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; // 应该是错误,但不是 ❌
foo2 = foo1; // 错误 - 正确 ✅提供显式注解可以加速这些循环处的类型检查并提供更好的准确性。 例如,在上面的例子中将 T 标记为不变有助于阻止有问题的赋值。
- type Foo<T> = {
+ type Foo<in out T> = {
x: T;
f: Bar<T>;
}我们不一定建议为每个类型参数注释其方差; 例如,使方差比必要的更严格是可能的(但不推荐),所以如果它实际上是协变、逆变甚至独立,TypeScript 不会阻止你将某物标记为不变。 因此,如果你选择添加显式方差标记,我们鼓励你深思熟虑并精确地使用它们。
但如果你正在处理深度递归类型,尤其是如果你是库作者,你可能有兴趣使用这些注释来让你的用户受益。 这些注释可以在精度和类型检查速度方面带来好处,甚至可以影响他们的代码编辑体验。 确定方差计算何时是类型检查时间的瓶颈可以通过实验进行,并使用像我们的 analyze-trace 工具来确定。
有关此功能的更多详细信息,你可以阅读拉取请求。
使用 moduleSuffixes 自定义解析
TypeScript 4.7 现在支持 moduleSuffixes 选项来自定义模块说明符的查找方式。
{
"compilerOptions": {
"moduleSuffixes": [".ios", ".native", ""]
}
}给定上述配置,如下所示的导入...
import * as foo from "./foo";将尝试查看相对文件 ./foo.ios.ts、./foo.native.ts,最后是 ./foo.ts。
此功能对于 React Native 项目很有用,其中每个目标平台可以使用具有不同 moduleSuffixes 的单独 tsconfig.json。
moduleSuffixes 选项由 Adam Foxman 贡献!
resolution-mode
使用 Node 的 ECMAScript 解析,包含文件的模式和使用的语法决定了导入的解析方式; 但是,从 ECMAScript 模块引用 CommonJS 模块的类型,反之亦然,将很有用。
TypeScript 现在允许 /// <reference types="..." /> 指令。
/// <reference types="pkg" resolution-mode="require" />
// 或
/// <reference types="pkg" resolution-mode="import" />此外,在 TypeScript 的夜间版本中,import type 可以指定导入断言来实现类似的功能。
// 就像使用 `require()` 导入一样解析 `pkg`
import type { TypeFromRequire } from "pkg" assert {
"resolution-mode": "require"
};
// 就像使用 `import` 导入一样解析 `pkg`
import type { TypeFromImport } from "pkg" assert {
"resolution-mode": "import"
};
export interface MergedType extends TypeFromRequire, TypeFromImport {}这些导入断言也可以在 import() 类型上使用。
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。 你可能会收到如下错误:
Resolution mode assertions are unstable. Use nightly TypeScript to silence this error. Try updating with 'npm install -D typescript@next'.如果你确实在 TypeScript 的夜间版本中使用此功能,请考虑就此问题提供反馈。
跳转到源定义
TypeScript 4.7 包含对一个名为跳转到源定义的新实验性编辑器命令的支持。 它类似于跳转到定义,但从不返回声明文件内的结果。 相反,它尝试查找相应的实现文件(如 .js 或 .ts 文件),并在那里找到定义——即使这些文件通常被 .d.ts 文件遮蔽。
当你需要查看从库导入的函数的实现,而不是其在 .d.ts 文件中的类型声明时,这通常非常有用。

你可以在最新版本的 Visual Studio Code 中尝试这个新命令。 但请注意,此功能仍处于预览阶段,并且有一些已知限制。 在某些情况下,TypeScript 使用启发式方法来猜测哪个 .js 文件对应于定义的给定结果,因此这些结果可能不准确。 Visual Studio Code 也尚未指示结果是否为猜测,但这是我们正在合作的事情。
你可以在我们的专用反馈问题上留下关于该功能的反馈、阅读已知限制或了解更多信息。
分组感知的组织导入
TypeScript 为 JavaScript 和 TypeScript 提供了组织导入编辑器功能。 不幸的是,它可能有点生硬,并且通常只会天真地对导入语句进行排序。
例如,如果你在以下文件上运行组织导入...
// 本地代码
import * as bbb from "./bbb";
import * as ccc from "./ccc";
import * as aaa from "./aaa";
// 内置模块
import * as path from "path";
import * as child_process from "child_process"
import * as fs from "fs";
// 一些代码...你会得到类似以下的内容
// 本地代码
import * as child_process from "child_process";
import * as fs from "fs";
// 内置模块
import * as path from "path";
import * as aaa from "./aaa";
import * as bbb from "./bbb";
import * as ccc from "./ccc";
// 一些代码...这...不理想。 当然,我们的导入是按路径排序的,并且我们的注释和换行符被保留,但并不是我们期望的方式。 很多时候,如果我们以特定方式对导入进行分组,那么我们希望保持这种分组。
TypeScript 4.7 以分组感知的方式执行组织导入。 在上面的代码上运行它看起来更像你期望的那样:
// 本地代码
import * as aaa from "./aaa";
import * as bbb from "./bbb";
import * as ccc from "./ccc";
// 内置模块
import * as child_process from "child_process";
import * as fs from "fs";
import * as path from "path";
// 一些代码...对象方法代码片段补全
TypeScript 现在为对象字面量方法提供代码片段补全。 当在对象中补全成员时,TypeScript 将仅为方法名称提供一个典型的补全条目,以及为完整的方法定义提供一个单独的补全条目!

有关更多详细信息,请参阅实现拉取请求。
破坏性更改
lib.d.ts 更新
虽然 TypeScript 努力避免重大破坏,但即使内置库中的微小更改也可能导致问题。 我们预计 DOM 和 lib.d.ts 更新不会导致重大破坏,但可能会有一些小问题。
JSX 中更严格的展开检查
在 JSX 中编写 ...spread 时,TypeScript 现在强制执行更严格的检查,即给定类型实际上是一个对象。 因此,具有 unknown 和 never 类型的值(以及更罕见的,仅仅是 null 和 undefined)不能再展开到 JSX 元素中。
所以对于以下示例:
import * as React from "react";
interface Props {
stuff?: string;
}
function MyComponent(props: unknown) {
return <div {...props} />;
}你现在将收到如下错误:
Spread types may only be created from object types.这使得此行为与对象字面量中的展开更加一致。
有关更多详细信息,请查看 GitHub 上的更改。
模板字符串表达式的更严格检查
当在模板字符串中使用 symbol 值时,它将在 JavaScript 中触发运行时错误。
let str = `hello ${Symbol()}`;
// TypeError: Cannot convert a Symbol value to a string因此,TypeScript 也会发出错误; 然而,TypeScript 现在还检查以某种方式约束为 symbol 的泛型值是否在模板字符串中使用。
function logKey<S extends string | symbol>(key: S): S {
// 现在是错误。
console.log(`${key} is the key`);
return key;
}
function get<T, K extends keyof T>(obj: T, key: K) {
// 现在是错误。
console.log(`Grabbing property '${key}'.`);
return obj[key];
}TypeScript 现在将发出以下错误:
Implicit conversion of a 'symbol' to a 'string' will fail at runtime. Consider wrapping this expression in 'String(...)'.在某些情况下,你可以通过将表达式包装在对 String 的调用中来解决此问题,正如错误消息所建议的那样。
function logKey<S extends string | symbol>(key: S): S {
// 不再是错误。
console.log(`${String(key)} is the key`);
return key;
}在其他情况下,此错误过于迂腐,并且在 keyof 时你可能根本不在乎是否允许 symbol 键。 在这种情况下,你可以切换到 string & keyof ...:
function get<T, K extends string & keyof T>(obj: T, key: K) {
// 不再是错误。
console.log(`Grabbing property '${key}'.`);
return obj[key];
}有关更多信息,你可以查看实现拉取请求。
LanguageServiceHost 上的 readFile 方法不再是可选的
如果你正在创建 LanguageService 实例,那么提供的 LanguageServiceHost 将需要提供 readFile 方法。 此更改对于支持新的 moduleDetection 编译器选项是必要的。
你可以在此处阅读有关更改的更多信息。
readonly 元组具有 readonly 的 length 属性
readonly 元组现在将其 length 属性视为 readonly。 对于固定长度的元组,这几乎从未被观察到,但对于具有尾随可选和剩余元素类型的元组来说,这是一个可以被观察到的疏忽。
因此,以下代码现在将失败:
function overwriteLength(tuple: readonly [string, string, string]) {
// 现在报错。
tuple.length = 7;
}你可以在此处阅读有关此更改的更多信息。