Skip to content
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待

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 设置添加了此功能:node16nodenext

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

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

package.json 中的 type 和新扩展名

Node.js 支持 package.json 中的一个新设置 叫做 type"type" 可以设置为 "module""commonjs"

jsonc
{
    "name": "my-package",
    "type": "module",

    "//": "...",
    "dependencies": {
    }
}

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

  • 可以使用 import/export 语句。
  • 可以使用顶层 await
  • 相对导入路径需要完整的扩展名(我们必须写 import "./foo.js" 而不是 import "./foo")。
  • 导入可能以不同于 node_modules 中依赖项的方式解析。
  • 某些类似全局的值(如 requiremodule)不能直接使用。
  • 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 文件之间的路径解析方式不同。 例如,假设你今天有以下代码:

ts
// ./foo.ts
export function helper() {
    // ...
}

// ./bar.ts
import { helper } from "./foo"; // 仅在 CJS 中有效

helper();

这段代码在 CommonJS 模块中有效,但在 ES 模块中会失败,因为相对导入路径需要使用扩展名。 因此,它必须被重写为使用 foo.ts 输出的扩展名——所以 bar.ts 将不得不从 ./foo.js 导入。

ts
// ./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 模块一样。

ts
// ./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 "...")。

ts
// ./foo.cts
export function helper() {
    console.log("hello world!");
}

// ./bar.mts
import { helper } from "./foo.cjs";

// 打印 "hello world!"
helper();

TypeScript 并不总能知道这些命名导出是否会被合成,但 TypeScript 在从明确是 CommonJS 模块的文件导入时会保持宽容并使用一些启发式方法。

关于互操作的一个 TypeScript 特定注意事项是以下语法:

ts
import foo = require("foo");

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

ts
// ./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

jsonc
// 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" 导入条件。

jsonc
// 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 在文件中发现任何 importexport 语句时,它就会将该文件视为模块,否则将假定 .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 不仅会查找 importexport 语句,还会检查

  • --module nodenext/--module node16 下运行时,package.json 中的 "type" 字段是否设置为 "module",以及
  • --jsx react-jsx 下运行时,当前文件是否为 JSX 文件

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

同时,"legacy" 选项只是回到旧行为,仅通过寻找 importexport 语句来确定文件是否为模块。

你可以在拉取请求上阅读更多关于此更改的信息

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

TypeScript 4.7 现在在索引键是字面量类型和唯一符号时收窄元素访问的类型。 例如,考虑以下代码:

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() 会触发错误。

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

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

ts
// '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 现在可以对对象和数组中的函数执行更细粒度的推断。 这允许这些函数的类型以从左到右的方式一致地流动,就像普通参数一样。

ts
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 函数。

ts
interface Box<T> {
    value: T;
}

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

也许我们想创建一组更专门的函数来创建 WrenchHammerBox。 今天要这样做,我们必须将 makeBox 包装在其他函数中,或者为 makeBox 的别名使用显式类型。

ts
function makeHammerBox(hammer: Hammer) {
    return makeBox(hammer);
}

// 或...

const makeWrenchBox: (wrench: Wrench) => Box<Wrench> = makeBox;

这些方法有效,但包装对 makeBox 的调用有点浪费,并且编写 makeWrenchBox 的完整签名可能变得笨拙。 理想情况下,我们希望能够在替换其签名中所有泛型的同时,简单地给 makeBox 起一个别名。

TypeScript 4.7 正是允许这样做的! 我们现在可以获取函数和构造函数,并直接为它们提供类型参数。

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

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

ts
const makeStringBox = makeBox<string>;

// TypeScript 正确地拒绝了这一点。
makeStringBox(42);

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

ts
// 具有类型 `new () => Map<string, Error>`
const ErrorMap = Map<string, Error>;

// 具有类型 `// Map<string, Error>`
const errorMap = new ErrorMap();

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

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

infer 类型变量上的 extends 约束

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

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 编写如下:

ts
type FirstIfString<T> =
    T extends [string, ...unknown[]]
        // 从 `T` 中获取第一个类型
        ? T[0]
        : never;

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

使用嵌套条件类型来推断类型然后匹配该推断类型是很常见的。 为了避免第二层嵌套,TypeScript 4.7 现在允许你在任何 infer 类型上放置约束。

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

这样,当 TypeScript 匹配 S 时,它也确保 S 必须是 string。 如果 S 不是 string,它会走 false 路径,在这些情况下是 never

有关更多详细信息,你可以在 GitHub 上阅读有关更改的信息

类型参数的可选方差注解

让我们看以下类型。

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 的每个类型都以相同的“方向”关联,我们说 Getter 类型在 T 上是协变的。 另一方面,检查 Setter<Dog>Setter<Animal> 是否有效涉及检查 AnimalDog 是否有效。 这种方向的“翻转”有点像数学中检查 −x < −y 与检查 y < x 相同。 当我们必须像这样翻转方向来比较 T 时,我们说 SetterT 上是逆变的

使用 TypeScript 4.7,我们现在能够显式地指定类型参数上的方差。

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

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

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

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

这里使用 outin 是因为类型参数的方差取决于它是用在输出还是输入位置。 与其考虑方差,不如只考虑 T 是否用在输出和输入位置。

也有同时使用 inout 的情况。

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

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

现在从技术上讲,在纯结构类型系统中,类型参数及其方差并不真正重要——你只需将类型插入每个类型参数的位置,并检查每个匹配成员在结构上是否兼容。 那么,如果 TypeScript 使用结构类型系统,我们为什么对类型参数的方差感兴趣? 我们为什么想要注释它们?

一个原因是,对于读者来说,能够一眼看到类型参数的使用方式可能很有用。 对于更复杂的类型,可能很难判断一个类型是用于读取、写入还是两者兼有。 如果我们忘记说明该类型参数的使用方式,TypeScript 也会帮助我们。 例如,如果我们忘记在 State 上同时指定 inout,我们会得到一个错误。

ts
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 已经尝试将类型参数的方差推断作为一种优化。 通过这样做,它可以在合理的时间内对较大的结构类型进行类型检查。 提前计算方差使类型检查器能够跳过更深的比较,而只需比较类型参数,这比一遍又一遍地比较类型的完整结构快得多。 但通常存在这种情况,这种计算仍然相当昂贵,并且计算可能会发现无法准确解析的循环,这意味着类型的方差没有明确的答案。

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;  // 应该是错误,但不是 ❌
foo2 = foo1;  // 错误 - 正确 ✅

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

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

我们不一定建议为每个类型参数注释其方差; 例如,使方差比必要的更严格是可能的(但不推荐),所以如果它实际上是协变、逆变甚至独立,TypeScript 不会阻止你将某物标记为不变。 因此,如果你选择添加显式方差标记,我们鼓励你深思熟虑并精确地使用它们。

但如果你正在处理深度递归类型,尤其是如果你是库作者,你可能有兴趣使用这些注释来让你的用户受益。 这些注释可以在精度和类型检查速度方面带来好处,甚至可以影响他们的代码编辑体验。 确定方差计算何时是类型检查时间的瓶颈可以通过实验进行,并使用像我们的 analyze-trace 工具来确定。

有关此功能的更多详细信息,你可以阅读拉取请求

使用 moduleSuffixes 自定义解析

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

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

给定上述配置,如下所示的导入...

ts
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="..." /> 指令。

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

// 或

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

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

ts
// 就像使用 `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() 类型上使用。

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。 你可能会收到如下错误:

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 文件中的类型声明时,这通常非常有用。

在 yargs 包的使用上使用“跳转到源定义”命令,编辑器跳转到 yargs 中的 index.cjs 文件。

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

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

分组感知的组织导入

TypeScript 为 JavaScript 和 TypeScript 提供了组织导入编辑器功能。 不幸的是,它可能有点生硬,并且通常只会天真地对导入语句进行排序。

例如,如果你在以下文件上运行组织导入...

ts
// 本地代码
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";

// 一些代码...

你会得到类似以下的内容

ts
// 本地代码
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 以分组感知的方式执行组织导入。 在上面的代码上运行它看起来更像你期望的那样:

ts
// 本地代码
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";

// 一些代码...

我们要感谢 Minh Quy 提供了此功能

对象方法代码片段补全

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

从对象补全一个完整的方法签名

有关更多详细信息,请参阅实现拉取请求

破坏性更改

lib.d.ts 更新

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

JSX 中更严格的展开检查

在 JSX 中编写 ...spread 时,TypeScript 现在强制执行更严格的检查,即给定类型实际上是一个对象。 因此,具有 unknownnever 类型的值(以及更罕见的,仅仅是 nullundefined)不能再展开到 JSX 元素中。

所以对于以下示例:

tsx
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 中触发运行时错误。

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

因此,TypeScript 也会发出错误; 然而,TypeScript 现在还检查以某种方式约束为 symbol 的泛型值是否在模板字符串中使用。

ts
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 的调用中来解决此问题,正如错误消息所建议的那样。

ts
function logKey<S extends string | symbol>(key: S): S {
    // 不再是错误。
    console.log(`${String(key)} is the key`);
    return key;
}

在其他情况下,此错误过于迂腐,并且在 keyof 时你可能根本不在乎是否允许 symbol 键。 在这种情况下,你可以切换到 string & keyof ...

ts
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 元组具有 readonlylength 属性

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

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

ts
function overwriteLength(tuple: readonly [string, string, string]) {
    // 现在报错。
    tuple.length = 7;
}

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