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

TypeScript 4.5

支持从 node_modules 引入 lib

为了确保 TypeScript 和 JavaScript 支持开箱即用,TypeScript 捆绑了一系列声明文件(.d.ts 文件)。 这些声明文件代表了 JavaScript 语言中可用的 API,以及标准的浏览器 DOM API。 虽然根据你的 target 有一些合理的默认值,但你可以通过在 tsconfig.json 中配置 lib 设置来挑选你的程序使用哪些声明文件。

将这些声明文件与 TypeScript 捆绑在一起有两个偶尔出现的缺点:

  • 当你升级 TypeScript 时,你也被迫处理 TypeScript 内置声明文件的更改,当 DOM API 频繁变化时,这可能是一个挑战。
  • 很难定制这些文件以匹配你的项目依赖项的需求(例如,如果你的依赖项声明它们使用 DOM API,你也可能被迫使用 DOM API)。

TypeScript 4.5 引入了一种方法,以类似于 @types/ 支持的方式覆盖特定的内置 lib。 当决定 TypeScript 应包含哪些 lib 文件时,它将首先在 node_modules 中查找作用域为 @typescript/lib-* 的包。 例如,当在 lib 中包含 dom 作为一个选项时,TypeScript 将使用 node_modules/@typescript/lib-dom 中的类型(如果可用)。

然后你可以使用包管理器安装一个特定的包来接管给定的 lib。 例如,今天 TypeScript 在 @types/web 上发布了 DOM API 的版本。 如果你想将项目锁定到特定版本的 DOM API,你可以将其添加到 package.json 中:

json
{
  "dependencies": {
    "@typescript/lib-dom": "npm:@types/web"
  }
}

然后从 4.5 开始,你可以更新 TypeScript,并且你的包管理器的 lockfile 将确保它使用完全相同版本的 DOM 类型。 这意味着你可以按照自己的节奏更新类型。

我们要特别感谢 saschanaz,在我们构建和试验此功能的过程中,他提供了极大的帮助和耐心。

有关更多信息,你可以查看此更改的实现

Awaited 类型和 Promise 改进

TypeScript 4.5 引入了一个新的实用类型,称为 Awaited 类型。 此类型旨在模拟 async 函数中的 await 操作,或 Promise 上的 .then() 方法——具体来说,就是它们递归地展开 Promise 的方式。

ts
// A = string
type A = Awaited<Promise<string>>;

// B = number
type B = Awaited<Promise<Promise<number>>>;

// C = boolean | number
type C = Awaited<boolean | Promise<number>>;

Awaited 类型对于模拟现有 API(包括 JavaScript 内置函数如 Promise.allPromise.race 等)非常有用。 事实上,Promise.all 的推断问题正是 Awaited 的动机之一。 以下示例在 TypeScript 4.4 及更早版本中失败。

ts
declare function MaybePromise<T>(value: T): T | Promise<T> | PromiseLike<T>;

async function doSomething(): Promise<[number, number]> {
  const result = await Promise.all([MaybePromise(100), MaybePromise(200)]);

  // 错误!
  //
  //    [number | Promise<100>, number | Promise<200>]
  //
  // 不能赋值给类型
  //
  //    [number, number]
  return result;
}

现在,Promise.all 利用某些特性与 Awaited 的组合,提供了更好的推断结果,上面的示例现在可以工作了。

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

模板字符串类型作为可辨识属性

TypeScript 4.5 现在可以收窄具有模板字符串类型的值,并且还将模板字符串类型识别为可辨识属性。

例如,以下代码以前会失败,但现在在 TypeScript 4.5 中成功通过类型检查。

ts
export interface Success {
    
type
: `${string}Success`;
body
: string;
} export interface Error {
type
: `${string}Error`;
message
: string
} export function
handler
(
r
: Success | Error) {
if (
r
.
type
=== "HttpSuccess") {
const
token
=
r
.
body
;
} }
Try

有关更多信息,请查看启用此功能的更改

module es2022

感谢 Kagami S. Rosylight,TypeScript 现在支持一个新的 module 设置:es2022module es2022 的主要特性是顶层 await,这意味着你可以在 async 函数之外使用 await。 这在 --module esnext(以及现在的 --module nodenext)中已经得到支持,但 es2022 是此功能的第一个稳定目标。

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

条件类型上的尾递归消除

当 TypeScript 检测到可能无限递归或任何可能花费很长时间并影响编辑器体验的类型扩展时,它通常需要优雅地失败。 因此,TypeScript 有启发式方法来确保在尝试解析无限深度的类型或处理生成大量中间结果的类型时不会失控。

ts
type InfiniteBox<T> = { item: InfiniteBox<T> };

type Unpack<T> = T extends { item: infer U } ? Unpack<U> : T;

// 错误:类型实例化过深且可能无限。
type Test = Unpack<InfiniteBox<number>>;

上面的示例故意简单且无用,但有很多类型实际上是有用的,但不幸触发了我们的启发式方法。 例如,下面的 TrimLeft 类型从类字符串类型的开头删除空格。 如果给定的字符串类型开头有空格,它会立即将字符串的其余部分反馈给 TrimLeft

ts
type TrimLeft<T extends string> =
    T extends ` ${infer Rest}` ? TrimLeft<Rest> : T;

// Test = "hello" | "world"
type Test = TrimLeft<"   hello" | " world">;

这种类型可能很有用,但如果一个字符串有 50 个前导空格,你会得到一个错误。

ts
type TrimLeft<T extends string> =
    T extends ` ${infer Rest}` ? TrimLeft<Rest> : T;

// 错误:类型实例化过深且可能无限。
type Test = TrimLeft<"                                                oops">;

这很遗憾,因为这类类型在模拟字符串操作(例如 URL 路由器的解析器)时非常有用。 更糟糕的是,更有用的类型通常会创建更多的类型实例化,从而对输入长度有更多限制。

但有一个可取之处:TrimLeft 以一种在一个分支中是尾递归的方式编写。 当它再次调用自身时,它会立即返回结果,而不对其进行任何操作。 因为这些类型不需要创建任何中间结果,所以它们可以更快速地实现,并且避免触发 TypeScript 内置的许多类型递归启发式方法。

这就是为什么 TypeScript 4.5 在条件类型上执行一些尾递归消除。 只要条件类型的一个分支只是另一个条件类型,TypeScript 就可以避免中间实例化。 仍然有启发式方法来确保这些类型不会失控,但它们更加宽松。

请记住,以下类型不会被优化,因为它通过将条件类型的结果添加到联合中来使用它。

ts
type GetChars<S> =
    S extends `${infer Char}${infer Rest}` ? Char | GetChars<Rest> : never;

如果你希望使其尾递归,可以引入一个辅助类型,该辅助类型带有一个“累加器”类型参数,就像尾递归函数一样。

ts
type GetChars<S> = GetCharsHelper<S, never>;
type GetCharsHelper<S, Acc> =
    S extends `${infer Char}${infer Rest}` ? GetCharsHelper<Rest, Char | Acc> : Acc;

你可以在此处阅读更多关于实现的信息。

禁用导入省略

在某些情况下,TypeScript 无法检测到你正在使用导入。 例如,考虑以下代码:

ts
import { Animal } from "./animal.js";

eval("console.log(new Animal().isDangerous())");

默认情况下,TypeScript 总是删除此导入,因为它似乎未被使用。 在 TypeScript 4.5 中,你可以启用一个名为 preserveValueImports 的新标志,以防止 TypeScript 从 JavaScript 输出中剥离任何导入的值。 使用 eval 的好理由很少,但在 Svelte 中发生了非常类似的事情:

html
<!-- 一个 .svelte 文件 -->
<script>
  import { someFunc } from "./some-module.js";
</script>

<button on:click="{someFunc}">Click me!</button>

以及在 Vue.js 中,使用其 <script setup> 功能:

html
<!-- 一个 .vue 文件 -->
<script setup>
  import { someFunc } from "./some-module.js";
</script>

<button @click="someFunc">Click me!</button>

这些框架根据 <script> 标签之外的标记生成一些代码,但 TypeScript 看到 <script> 标签内的代码。 这意味着 TypeScript 将自动删除对 someFunc 的导入,上述代码将无法运行! 使用 TypeScript 4.5,你可以使用 preserveValueImports 来避免这些情况。

请注意,此标志与 --isolatedModules` 结合使用时有一个特殊要求:导入的类型必须标记为仅类型,因为一次处理一个文件的编译器无法知道导入是看似未使用的值,还是为了避免运行时崩溃而必须删除的类型。

ts
// 哪些是应该保留的值?tsc 知道,但 `ts.transpileModule`、
// ts-loader、esbuild 等不知道,因此 `isolatedModules` 会报错。
import { someFunc, BaseType } from "./some-module.js";
//                 ^^^^^^^^
// 错误:'BaseType' 是一个类型,当同时启用 'preserveValueImports' 和 'isolatedModules' 时,必须使用仅类型导入导入。

这使得 TypeScript 4.5 的另一个特性——导入名称上的 type 修饰符变得尤为重要。

有关更多信息,请在此处查看拉取请求

导入名称上的 type 修饰符

如上所述,preserveValueImportsisolatedModules 有特殊要求,以便构建工具明确是否安全地删除类型导入。

ts
// 哪些是应该保留的值?tsc 知道,但 `ts.transpileModule`、
// ts-loader、esbuild 等不知道,因此 `isolatedModules` 会报错。
import { someFunc, BaseType } from "./some-module.js";
//                 ^^^^^^^^
// 错误:'BaseType' 是一个类型,当同时启用 'preserveValueImports' 和 'isolatedModules' 时,必须使用仅类型导入导入。

当这些选项组合使用时,我们需要一种方法来指示何时可以合法地删除导入。 TypeScript 已经通过 import type 实现了这一点:

ts
import type { BaseType } from "./some-module.js";
import { someFunc } from "./some-module.js";

export class Thing implements BaseType {
  // ...
}

这有效,但为同一模块使用两个导入语句并不理想。 这就是为什么 TypeScript 4.5 允许在单个命名导入上使用 type 修饰符,以便你可以根据需要混合和匹配。

ts
import { someFunc, type BaseType } from "./some-module.js";

export class Thing implements BaseType {
    someMethod() {
        someFunc();
    }
}

在上面的示例中,BaseType 保证被删除,而 someFunc 将在 preserveValueImports 下保留,留下以下代码:

js
import { someFunc } from "./some-module.js";

export class Thing {
  someMethod() {
    someFunc();
  }
}

有关更多信息,请查看 GitHub 上的更改

私有字段存在性检查

TypeScript 4.5 支持一个 ECMAScript 提案,用于检查对象是否具有私有字段。 现在,你可以编写一个带有 #private 字段成员的类,并通过使用 in 运算符来查看另一个对象是否具有相同的字段。

ts
class Person {
    #name: string;
    constructor(name: string) {
        this.#name = name;
    }

    equals(other: unknown) {
        return other &&
            typeof other === "object" &&
            #name in other && // <- 这是新的!
            this.#name === other.#name;
    }
}

此功能的一个有趣之处在于,检查 #name in other 意味着 other 必须已经被构造为 Person,因为没有其他方式可以使该字段存在。 这实际上是该提案的关键特性之一,也是该提案被命名为“人体工程学品牌检查”的原因——因为私有字段通常充当一个“品牌”,以防止非其类实例的对象。 因此,TypeScript 能够在每次检查时适当地收窄 other 的类型,直到最终得到类型 Person

我们要特别感谢我们在彭博社的朋友们贡献了这个拉取请求Ashley ClaymoreTitian Cernicova-DragomirKubilay KahveciRob Palmer

导入断言

TypeScript 4.5 支持一个 ECMAScript 提案,用于导入断言。 这是一种由运行时使用的语法,以确保导入具有预期的格式。

ts
import obj from "./something.json" assert { type: "json" };

这些断言的内容不由 TypeScript 检查,因为它们是宿主特定的,并且只是保留原样,以便浏览器和运行时可以处理它们(并可能报错)。

ts
// TypeScript 对此没有问题。
// 但你的浏览器呢?可能不行。
import obj from "./something.json" assert {
    type: "fluffy bunny"
};

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

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

第二个参数的预期类型由一个新类型 ImportCallOptions 定义,目前只接受一个 assert 属性。

我们要感谢 Wenlu Wang 实现了此功能

JSDoc 中的常量断言和默认类型参数

TypeScript 4.5 为我们的 JSDoc 支持带来了一些额外的表达能力。

其中一个例子是 const 断言。在 TypeScript 中,你可以在字面量后写入 as const 以获得更精确且不可变的类型。

ts
// 类型为 { prop: string }
let a = { prop: "hello" };

// 类型为 { readonly prop: "hello" }
let b = { prop: "hello" } as const;

在 JavaScript 文件中,你现在可以使用 JSDoc 类型断言来实现相同的目的。

ts
// 类型为 { prop: string }
let a = { prop: "hello" };

// 类型为 { readonly prop: "hello" }
let b = /** @type {const} */ ({ prop: "hello" });

提醒一下,JSDoc 类型断言注释以 /** @type {TheTypeWeWant} */ 开头,后跟一个带括号的表达式:

js
/** @type {TheTypeWeWant} */` (someExpression)

TypeScript 4.5 还为 JSDoc 添加了默认类型参数,这意味着 TypeScript 中的以下 type 声明:

ts
type Foo<T extends string | number = number> = { prop: T };

可以在 JavaScript 中重写为以下 @typedef 声明:

js
/**
 * @template {string | number} [T=number]
 * @typedef Foo
 * @property prop {T}
 */

// 或

/**
 * @template {string | number} [T=number]
 * @typedef {{ prop: T }} Foo
 */

有关更多信息,请查看常量断言的拉取请求以及类型参数默认值的更改

使用 realPathSync.native 加快加载速度

TypeScript 现在在所有操作系统上都利用了 Node.js realPathSync 函数的系统原生实现。

以前,此函数仅在 Linux 上使用,但在 TypeScript 4.5 中,它已被用于通常不区分大小写的操作系统,如 Windows 和 MacOS。 在某些代码库上,此更改将项目加载速度提高了 5-13%(取决于主机操作系统)。

有关更多信息,请在此处查看原始更改,以及在此处查看 4.5 特定的更改

JSX 属性的代码片段补全

TypeScript 4.5 为 JSX 属性带来了代码片段补全。 当在 JSX 标签中编写属性时,TypeScript 已经会为这些属性提供建议; 但通过代码片段补全,它们可以通过添加初始化器并将光标放在正确的位置来减少一些额外的输入。

JSX 属性的代码片段补全。对于字符串属性,会自动添加引号。对于数字属性,会添加大括号。

TypeScript 通常使用属性的类型来确定插入哪种初始化器,但你可以在 Visual Studio Code 中自定义此行为。

VS Code 中 JSX 属性补全的设置

请注意,此功能仅适用于较新版本的 Visual Studio Code,因此你可能需要使用 Insiders 版才能使其工作。 有关更多信息,请阅读原始拉取请求

对未解析类型的更好编辑器支持

在某些情况下,编辑器会利用轻量级的“部分”语义模式——要么在等待完整项目加载时,要么在像 GitHub 的基于 Web 的编辑器这样的上下文中。

在旧版本的 TypeScript 中,如果语言服务找不到类型,它只会打印 any

在 Buffer 未找到的签名上悬停,TypeScript 将其替换为 any。

在上面的示例中,未找到 Buffer,因此 TypeScript 在快速信息中将其替换为 any。 在 TypeScript 4.5 中,TypeScript 将尽力保留你编写的内容。

在 Buffer 未找到的签名上悬停,它继续使用名称 Buffer。

但是,如果你在 Buffer 本身上悬停,你会得到 TypeScript 找不到 Buffer 的提示。

TypeScript 显示 type Buffer = /* unresolved */ any;

总之,当 TypeScript 没有完整的程序可用时,这提供了更流畅的体验。 请记住,在常规场景中,你总是会收到错误,以告诉你何时找不到类型。

有关更多信息,请在此处查看实现

破坏性更改

lib.d.ts 更改

TypeScript 4.5 包含对其内置声明文件的更改,这可能会影响你的编译; 然而,这些更改相当微小,我们预计大多数代码不会受到影响。

Awaited 带来的推断更改

由于 Awaited 现在在 lib.d.ts 中使用,并且作为 await 的结果,你可能会看到某些泛型类型发生变化,这可能导致不兼容; 然而,考虑到围绕 Awaited 的许多故意设计决策是为了避免破坏,我们预计大多数代码不会受到影响。

tsconfig.json 根部的编译器选项检查

很容易犯错误,忘记在 tsconfig.json 中包含 compilerOptions 部分。 为了帮助捕获此错误,在 TypeScript 4.5 中,如果在没有同时在该 tsconfig.json 中定义 compilerOptions 的情况下,添加一个与 compilerOptions 中任何可用选项匹配的顶级字段,则视为错误。