【第2463期】TypeScript 4.5正式发布

前言

版本随着时间越来越高,将来大家还会关注它升了什么吗?比如jQuery。今日前端早读课文章由@Hugo翻译授权分享。

正文从这开始~~

介绍

今天,我们很高兴宣布,TypeScript 的 4.5 版本发布了!

如果你还对 TypeScript 不熟悉,TypeScript 是一门为了给 JavaScript 增加严格类型检查的编程语言。通过使用 TypeScript 的静态类型,你可以使用 TypeScript 的编译器去检查手误和编写的代码与你定义的数据格式的适配问题,同时,你还可以获得类型带来的便捷的提示。TypeScript 的类型不会改变你的 JavaScript 程序,事实上,你去除掉这些类型,就转化成了干净、可读的 JavaScript。除了帮助你减少 Bugs,TypeScript 也可以提升你的编辑器自动补齐代码、跳转定义和变量重命名的能力。如果想了解更多,请阅读官网。

你可以通过 NuGet 开始使用 TypeScript 4.5,或者通过 npm 的方式:

npm install typescript

TypeScript 4.5 比较重大的更新是:

自 Beta 和 Rc 版本以来,有什么变化?

自从 beta 发布博文和 RC 发布博文,4.5 有一些小变化。

最大的变化是,我们延期了本来计划支持的 Node.js 12 的 ECMAScript Module 原生支持,现在只作为一个实验性的特性在 nightly 版本发布。这是一个很难的决定,我们的团队对于整个 TypeScript 生态是否准备好以及如何和何时去使用这个特性做了很多讨论。我们期望这个发布可以获得更顺滑的用户体验,而不是让大多数人很难受。但是同时,你仍然可以在TypeScript nightly 版本 使用 --- module nodenest 和 --moduleResolution nodenext 获得这些实验性特性。如果你在 TypeScript 4.5 正式版使用这些特性,你会得到一个报错信息来引导你去用 nightly 版本。

自从我们 RC 版本,我们增加了一些新的 JSDoc 特性。实际上 RC 版本就包含这些功能,我们只是博文上没写。

从编辑器的角度,我们引入了比 beta 版本更好的自动补全功能,针对方法实现和重载。

我们也强调了在 --build 模式下的性能衰退,因为使用了 package.json 的 过多的 realpath 调用。这个变化影响了 TypeScript 4.5,但是我们也引入到了 TypeScript 4.4.4.如果这个性能衰退影响你尝试 TypeScript 4.4,你应该和之前的版本比较下 --build 模式。

具体改变

Awaited 和 Promise 类型增强(The Awaited Type and Promise Improvements)

TypeScript 4.5 引入了一个新的工具类型,名字叫 Awaited。这个类型用来描述在async函数中的 await 操作,或者是 Promise 的 .then() ,具体来说,在递归解包 Promise 。

// 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.all,Promise.race 等等。实际上,正是 Promise.all 的一些问题推动了 Awaited 类型的诞生。下面这个例子,在 TypeScript 4.4 和更早的版本是有问题的。

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)
]);

// Error!
//
// [number | Promise<100>, number | Promise<200>]
//
// is not assignable to type
//
// [number, number]
return result;
}

现在 Promise.all 可以使用 Awaited 来获得更好的结果,上述的例子就可以正常运行了。

对于这一点,如果你想知道更多,请阅读。

支持 node_modules 来使用 lib(Supporting lib from node_modules)

为了保证 TypeScript 和 JavaScript 可以更好的工作,TypeScript 打包了一系列的类型声明文件(.d.ts 文件)。这些声明文件是 JavaScript 可用的 APIs 以及标准浏览器 DOM 的 APIs。通过设置你编译到的目标参数 target 的不同,你可以选择你想使用的 lib 。

这里有两个偶尔存在的缺点来包含这些声明文件:

TypeScript 对于 lib 引入了一个覆盖机制,这个机制和 @types 的工作原理差不多。当 TypeScript 去决定哪个 lib 文件要被编译时引入,它会先去 node_modules 下先去查看@typescript/lib-* 的情况。举例来说,当包含 dom 作为一个选项在 lib 中,TypeScript 会去用 node_modules/@typescript/lib-dom 。

然后你就可以用你的包管理工具来安装一个特定的 lib。例如,目前 TypeScript 发布了 DOM APIs 在 @types/web。如果你想锁定你的项目在一个特定的 DOM APIs 的版本,你可以把这个加入到你的 package.json

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

这样,从 4.5 开始,你可以升级 TypeScript,同时你可以通过你的依赖管理的 lockfile 来保证 DOM 的 type lib 还是你之前指定的版本。也就是说,这些 lib 的管理权回到了你的手中。

我们非常感谢 saschanaz,他帮助我们实现了这个特性。

如果你对这个改动的细节感兴趣,请阅读。

模板字符串作为判别式(Template String Types as Discriminants)

TypeScript 4.5 可以收束拥有模板字符串类型的类型,也可以识别模板字符串作为判别式。

下面这个例子,在 4.5 之前是失败的。

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") {
// 'r' has type 'Success'
let token = r.body;
}
}

更细节的信息,请阅读。

--module es2022

感谢 Kagami S. Rosylight,TypeScript 现在支持新的 module 设置,es2022。--modulees2022 最主要增加了 top-level await,也代表你可以在 async 函数之外使用 await 了。这个特性在之前的 --module esnext(和 --module nodenext)已经支持了,但是 es2022是第一个稳定支持这个 target 的版本。

如果你想了解更多,请阅读。

条件类型的尾递归省略(Tail-Recursion Elimination on Conditional Types)

TypeScript 有时检查到无限递归后,需要优雅的报错,或者某个类型表达式持续太长时间影响了你的编辑器体验。TypeScript 在处理无限深度的类型和可能产生很多中间结果的类型时要保证事情能够顺利进行。

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

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

// error: Type instantiation is excessively deep and possibly infinite.
type Test = Unpack<InfiniteBox<number>>

上面这个简单的例子是无用的,但是有很多类似这样结构的例子是很有用的,但是仍然会触发我们的规则。下面这个例子,TrimLeft 类型移除了一个 string 相似类型的开头空格。如果给出的 string 类型的开头有空格,它会把余下的部分返回给 TrimLeft(也就是自己)。

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

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

这个类型是有用的,但是如果一个 string 有 50个空格,你会得到一个 error:

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

// error: Type instantiation is excessively deep and possibly infinite.
type Test = TrimLeft<" oops">;

这很不幸,因为这种类型在处理例如 URL 路由的 string 类型时,非常常用。更糟糕的是,如果你用这个类型去产生一些类型,会有更多的类型实例,就会可能产生更多的限制。

但是有更优雅的办法:TrimLeft 可以用尾递归的方式编写。当它自己调用自己的时候,它什么都不做直接返回结果。因为这些类型不用创建任何中间结果,这样实现可以更快,并且避免触碰到 TypeScript 内置的递归规则限制。

这是 TypeScript 4.5 在条件类型实现尾递归消除的原因。只要一个条件类型的分支是另一个条件类型,TypeScript 可以避免中间结果的实例化。这样,以前的规则限制依然存在,但是条件限制又放宽了很多。

记住,下面的类型是不会优化的,因为这些例子把条件类型的结果做了 union 操作。

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

如果你想把这个进行尾递归优化,你可以引入一个帮助类型来“积累”类型参数,就像尾递归函数一样。

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

你可以在这里读到详细的实现。

关闭引用省略(Disabling Import Elision)

有一些常见,TypeScript 不能检测到你用了一个 import 语法。例如:

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

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

默认情况下,TypeScript 会判断这个 import 没用而去掉它。在 TypeScript 4.5,你可以打开一个新的编译器配置 --preserveValueImports 来阻止 TypeScript 消除生成的 JavaScript 脚本中的 import 语句。虽然用 eval 的 场景很少见,但是和一些 Svelte 的场景类似:

<!-- A .svelte File -->
<script>
import { someFunc } from "./some-module.js";
</script>

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

在 Vue.js 中:

<!-- A .vue File -->
<script setup>
import { someFunc } from "./some-module.js";
</script>

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

这些框架会在 <script> 标签外边生成一些代码,但是 TypeScript 只能看到 <script> 标签里面的代码。这代表着,TypeScript 可以自动把 someFunc 的 import 消除掉,上述代码就会运行失败。在 TypeScript 4.5 中,你可以用 --preserveValueImports 来避免这些常见。

注意这个配置和 --isolatedModules 配合在一起有一些特殊的需求:引入的类型必须标记 type-only,因为 编译器每次只能处理一个单独的文件,编译器不知道引入的值没有用,或者这是一个类型,必须移除,否则会导致运行的问题。

// 引入的哪一个是要保留的值引用呢? tsc 知道, 但是 `ts.transpileModule`,
// ts-loader, esbuild, 等 不知道, 所以 `isolatedModules` 获得一个报错
import { someFunc, BaseType } from "./some-module.js";
// ^^^^^^^^
// Error: 'BaseType' is a type and must be imported using a type-only import
// 当 'preserveValueImports' 和 'isolatedModules' 一起打开时。

这实际上需要另一个 TypeScript 4.5 的特性,引用语句支持 type 修饰词。

针对这个特性,更详细的文章。

引用语句支持 type 修饰词(type Modifiers on Import Names)

就像上面提到的,--preserveValueImports 和--isolatedModules 有特殊的需要来阻止可能的模糊不清的场景,才能让建造工具知道是否可以安全的去除类型引入。

// 引入的哪一个是要保留的值引用呢? tsc 知道, 但是 `ts.transpileModule`,
// ts-loader, esbuild, 等 不知道, 所以 `isolatedModules` 获得一个报错
import { someFunc, BaseType } from "./some-module.js";
// ^^^^^^^^
// Error: 'BaseType' is a type and must be imported using a type-only import
// 当 'preserveValueImports' 和 'isolatedModules' 一起打开时。

当这类选项结合的时候,我们需要一个方法去标识可以安全合法的去除引入的方法。TypeScript 已经有 import type 的语法来支持这个事情。

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

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

这个是可行的,但是最好可以避免对于一个模块文件的两行 import 语句。这也是为什么 TypeScript 4.5 允许一个 type 修饰词在 import 语句中。这样你就可以一行语句支持这些功能。

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

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

在上面的例子里,BaseType 会保证被编译器消除,如果开启了 --preserveValueImports, someFunc 一定会保留。这样编译器会产生:

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

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

这一特性细节见文章。

object 类型支持私有成员检测(Private Field Presence Checks)

TypeScript 4.5 支持了一个 ECMAScript 的特性,检测是否一个 object 有私有字段。你可以通过 #private 语法来创建一个私有字段,然后用 in 操作符来检测这个 object 是否有私有成员。

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 的过程,因为只有这样,这个字段才存在。这实际上是这个提议最核心的功能之一,这也是为什么这个提案被称为“工效类型检查(ergonomic brand checks)”,因为私有字段进行扮演为一个 “类型(brand)”去保证一个实例是否是一个类的实例。这样,TypeScript 可以不断收束 other 的类型,直到 Person 类型。

我们非常感谢来自 Bloomberg 的朋友们贡献这个 PR: Ashley Claymore, Titian Cernicova-Dragomir, Kubilay Kahveci, and Rob Palmer!

导入断言(Import Assertions)

TypeScript 支持了一个新的 ECMAScript 的提议倒入断言。这个语法可以让运行时确保引入的 import 符合预期的格式。

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

这些断言不会被 TypeScript 执行,因为他们的目标是为了让浏览器和运行时可以处理他们以及潜在的错误。

// TypeScript 对这些代码是 ok 的。
// 但是你的浏览器,可能就要报错了。
import obj from "./something.json" assert {
type: "fluffy bunny"
};
动态 import() 也可以通过第二个参数使用这个断言。

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

第二个参数预期是实现一个叫做 ImportCallOptions 的新类型,目前这个类型之接受 assert 属性。

我们感谢 Wenlu Wang 实现了这个特性!

JSDoc 中的常量断言和默认类型参数(Const Assertions and Default Type Arguments in JSDoc)
TypeScript 4.5 让 JSDoc 获得了更多的表达能力。

一个例子是 const 断言。在 TypeScript 中,你可以通过在字面量后面加 as const 语法来让获得更准确和不可变的类型。

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

// 类型是 { readonly prop: "hello" }
let b = { prop: "hello" } as const;
在 JavaScript 中,你可以用 JSDoc 的类型来实现一样的事情。

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

// 类型是 { readonly prop: "hello" }
let b = /** @type {const} */ ({ prop: "hello" });
JSDoc 的类型断言语法是 /** @type {TheTypeWeWant} */,后面跟着一个括号表达式。

/** @type {TheTypeWeWant} */` (someExpression)
TypeScript 4.5 也让 JSDoc 支持了默认类型参数,代表着 TypeScript 的表达式:

type Foo<T extends string | number = number> = { prop: T };
可以用 @typedef 声明在 JavaScript 中实现:

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

// 或者

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

如果想知道更深的细节 const 断言的 PR 和 默认参数的 PR.

通过 realpathSync.native 实现更快的加载速度(Faster Load Time with realPathSync.native)

TypeScript 现在在所有的操作系统的 Node.js 环境里充分使用 realpathSync.native 函数。

之前,这个函数只在 Linux 环境里使用,但是在 TypeScript 4.5,只要你用的 Node.js 版本比较新,编译器就会在不区分大小写的操作系统,例如 Windows 和 MacOS,中使用这个函数。在 Windows 环境中,一个例子是这个优化带来了 5-13% 的性能优化。

更详细的信息 原始变化的 PR, 以及 4.5 改动的 PR。

新的代码补全(New Snippet Completions)

TypeScript 带来两个新的代码补全,这些自动补全可以自动产生一些默认的文本,允许你根据这个文本来进行调整。

类型方法自动补全(Snippet Completions for Methods in Classes)

TypeScript 4.5 现在提供当你重写或者实现一个类的方法时的自动提示。

当实现一个接口的方法,或者在子类中重写一个方法,TypeScript 可以补齐这个方法的名字,方法签名和方法体的括号。当你补齐你的代码,你的光标会自动跳转到方法体内。

这个特性的实现在这里。

JSX 属性的自动补全(Snippet Completions for JSX Attributes)

TypeScript 4.5 带来了 JSX 属性的自动补全。当编写 JSX 标签时,TypeScript 会提供这些属性的建议;通过自动补全,你可以通过声明和放置光标在合适的位置来保存一些你的额外类型。

TypeScript 会自动处理这些设置,你也可以自己在 Visual Studio Code 里设置。

这个特性只有在新版的 Visual Studio Code 中生效,你可以通过使用一个 Insiders 来让这个特性工作。这个 PR 在这里。

编辑器对未解析的类型增强(Better Editor Support for Unresolved Types)

在一些场景下,编辑器会进入轻量的“partial”语义模式,要么等待整个项目载入以后,或者像 GitHub 的Web 编辑器。

在老版本 TypeScript 中,如果语言服务没有发现一个类型,编辑器就会打印 any。

在上面的例子,Buffer 没有被找见,所以 TypeScript 把这个替换成了 any。在 TypeScript 4.5 中,TypeScript 会保留你写的东西。

然而,如果你把鼠标悬停在 Buffer 上,你还是会得到 TypeSript 没有找到 Buffer。

这个变动,让 TypeScript 在没有得到全部程序时,显示的更好。如果你没有找到一个类型,总是会报错的。

这个实现在这里。

Nightly 版本的 Node.js 的测试性 ECMAScript Module 支持(Experimental Nightly-Only ECMAScript Module Support in Node.js)

在过去的几年里,Node.js 一直在做支持 ECMAScript Modules(ESM)的工作。这是一个非常难支持的特性,因为 Node.js 的整个生态系统是建立在另一个叫做 CommonJS (CJS) 的模块管理系统上的。互相操作两者带来很多挑战。(真的很多。)

TypeScript 4.5 开始支持 Node.js 对 ESM 的原生支持;然而,我们相信,这个特性在更广泛应用前还需要一些“烹饪”时间。你可以在这个文章中找到原因。

所以,这个特性只在 nightly 版本中提供,而不是在正式版。

如果你对这个特性感兴趣,可以来看这个特性的一些文档, 来试一试吧

重大变化(Breaking Changes)
lib.d.ts 变化

TypeScript 4.5 包含一些内置类型声明文件的变化,可能会影响你的编译,但是,这些变化都比较小,我们期望不会影响绝大部分代码。

Awaited 的引用变化

因为 Awaited 类型会在 lib.d.ts 中作为 await 的结果来使用,你可以看到一些泛型的变化导致一些不兼容的问题。这个可能导致 Promise.all,Promise.allSettled 的一些类型的问题。

你可以通过移除这些类型参数来修复一些问题

- Promise.all<boolean, boolean>(...)
+ Promise.all(...)

更深入的例子,可能需要你把类型变量改为 tuple 型的类型。

- Promise.all<boolean, boolean>(...)
+ Promise.all<[boolean, boolean]>(...)

也有可能会有一些场景,做上述两种修改都不行。一个特殊的例子是当一个元素可能是 Promise 或者 non-Promise。在这种例子里,不能打开下层的元素类型。

- Promise.all<boolean | undefined, boolean | undefined>(...)
+ Promise.all<[Promise<boolean> | undefined, Promise<boolean> | undefined]>(...)
更严格的 tsconfig.json 根参数监测

在 tsconfig.json 里忘写 compilerOptions 是经常会发生的情况。为了防止这个问题,TypeScript 4.5 会在没有 complierOptions 字段的时候报错。

条件类型转移的限制

TypeScript 不在允许利用 infer 关键字进行类型转移,这个问题经常造成严重的性能问题。可以查看这个来获得更详细的信息

下一步

我们已经开始为了 TypeScript 4.6 工作!如果你感兴趣,可以来看 4.6 的路线图。在下一个版本里,我们计划更关注性能和稳定性。

同时,我们相信 TypeScript 4.5 能给你带来更多喜爱的东西,和现实生活的改善!我们希望这个发布可以让大家高兴。

Happy Hacking!

– Daniel Rosenwasser and the TypeScript Team

关于本文
译者:@Hugo
译文:https://zhuanlan.zhihu.com/p/435054926
作者:@Daniel
原文:
https://devblogs.microsoft.com/typescript/announcing-typescript-4-5/

为你推荐


【第2412期】TypeScript条件类型


【第2444期】可能是最完善的 React+Vite 解决方案,阿里飞冰团队发布 icejs 2.0 版本


欢迎自荐投稿VX:zhgb_f2er,前端早读课等你来。