TypeScript 5.2 發(fā)布,支持顯式資源管理!
根據(jù) TypeScript 路線圖,TypeScript 5.2 計(jì)劃于 8.22 發(fā)布。下面就來看看該版本都帶來了哪些新特性!
以下是 TypeScript 5.2 新增的功能:
- using 聲明和顯式資源管理
- 裝飾器元數(shù)據(jù)
- 命名和匿名元組元素
- 聯(lián)合類型數(shù)組方法調(diào)用
- 對(duì)象成員的逗號(hào)自動(dòng)補(bǔ)全
- 內(nèi)聯(lián)變量重構(gòu)
- 重大變更和正確性修復(fù)
using 聲明和顯式資源管理
TypeScript 5.2 添加了對(duì) ECMAScript 中即將推出的顯式資源管理功能的支持。
創(chuàng)建對(duì)象后通常需要進(jìn)行某種“清理”。 例如,可能需要關(guān)閉網(wǎng)絡(luò)連接、刪除臨時(shí)文件或只是釋放一些內(nèi)存空間。
假如有一個(gè)函數(shù),它創(chuàng)建了一個(gè)臨時(shí)文件,通過各種操作讀寫該文件,然后關(guān)閉并刪除它。
import * as fs from "fs";
export function doSomeWork() {
const path = ".some_temp_file";
const file = fs.openSync(path, "w+");
// 操作文件...
// 關(guān)閉文件并刪除它
fs.closeSync(file);
fs.unlinkSync(path);
}
那如果需要提前退出怎么辦?
export function doSomeWork() {
const path = ".some_temp_file";
const file = fs.openSync(path, "w+");
// 操作文件...
if (someCondition()) {
// 其他操作...
// 關(guān)閉文件并刪除它
fs.closeSync(file);
fs.unlinkSync(path);
return;
}
// 關(guān)閉文件并刪除它
fs.closeSync(file);
fs.unlinkSync(path);
}
可以看到,這里就出現(xiàn)了重復(fù)的清理工作。如果拋出錯(cuò)誤,我們也不保證關(guān)閉并刪除文件。這可以通過將這一切包裝在 try/finally 塊中來解決。
export function doSomeWork() {
const path = ".some_temp_file";
const file = fs.openSync(path, "w+");
try {
// 操作文件...
if (someCondition()) {
// 其他操作...
return;
}
}
finally {
// 關(guān)閉文件并刪除它
fs.closeSync(file);
fs.unlinkSync(path);
}
}
這樣寫雖然沒有什么大問題,但是會(huì)讓代碼變得復(fù)雜。如果我們向finally塊添加更多清理邏輯,還可能遇到其他問題,比如異常阻止了其他資源的釋放。 這就是顯式資源管理提案旨在解決的問題。該提案的關(guān)鍵思想是將資源釋放(要處理的清理工作)作為 JavaScript 中的一等公民來支持。
這一功能的實(shí)現(xiàn)方式是引入一個(gè)名為 Symbol.dispose 的新內(nèi)置 symbol,并且可以創(chuàng)建具有以 Symbol.dispose 命名的方法的對(duì)象。為了方便起見,TypeScript 定義了一個(gè)名為 Disposable 的新全局類型,用于描述這些對(duì)象。
class TempFile implements Disposable {
#path: string;
#handle: number;
constructor(path: string) {
this.#path = path;
this.#handle = fs.openSync(path, "w+");
}
// 其他操作
[Symbol.dispose]() {
// 關(guān)閉文件并刪除它
fs.closeSync(this.#handle);
fs.unlinkSync(this.#path);
}
}
然后,可以調(diào)用這些方法:
export function doSomeWork() {
const file = new TempFile(".some_temp_file");
try {
// ...
}
finally {
file[Symbol.dispose]();
}
}
將清理邏輯移到 TempFile 本身并不能帶來很大的好處;基本上只是將所有的清理工作從 finally 塊中移到一個(gè)方法中,而這之前這就是可以實(shí)現(xiàn)的。但是,有一個(gè)眾所周知的方法名稱意味著 JavaScript 可以在其之上構(gòu)建其他功能。
這就引出了該特性的第一個(gè)亮點(diǎn):使用 using 聲明!using 是一個(gè)新的關(guān)鍵字,可以聲明新的固定綁定,有點(diǎn)類似于 const。關(guān)鍵的區(qū)別在于,使用 using 聲明的變量在作用域結(jié)束時(shí)會(huì)調(diào)用其 Symbol.dispose 方法!
所以我們可以簡(jiǎn)單地編寫這段代碼:
export function doSomeWork() {
using file = new TempFile(".some_temp_file");
// 操作文件...
if (someCondition()) {
// 其他操作...
return;
}
}
可以看到,已經(jīng)沒有了 try/finally 塊,從功能上來說,這就是 using 聲明為我們做的事情,但我們不必處理其中的細(xì)節(jié)。
如果你對(duì) C# 的 using 聲明、Python 的 with 語句或者 Java 的 try-with-resource 聲明比較熟悉。就會(huì)發(fā)現(xiàn),JavaScript 的新關(guān)鍵字 using 和它們類似,并提供了一種顯式的在作用域結(jié)束時(shí)執(zhí)行對(duì)象的 "清理" 操作的方式。
**using**** 聲明會(huì)在其所屬的作用域的最后,或者在出現(xiàn) "早期返回"(如 return 或拋出錯(cuò)誤)之前進(jìn)行清理操作。它們還按照先進(jìn)后出的順序(類似于棧)進(jìn)行釋放。
function loggy(id: string): Disposable {
console.log(`Creating ${id}`);
return {
[Symbol.dispose]() {
console.log(`Disposing ${id}`);
}
}
}
function func() {
using a = loggy("a");
using b = loggy("b");
{
using c = loggy("c");
using d = loggy("d");
}
using e = loggy("e");
return;
// 不會(huì)執(zhí)行
// 不會(huì)創(chuàng)建,不會(huì)處置
using f = loggy("f");
}
func();
// Creating a
// Creating b
// Creating c
// Creating d
// Disposing d
// Disposing c
// Creating e
// Disposing e
// Disposing b
// Disposing a
使用 using 聲明的特點(diǎn)之一是在處理資源時(shí)具有異常處理的彈性。當(dāng)使用聲明結(jié)束時(shí),如果發(fā)生了錯(cuò)誤,該錯(cuò)誤將在資源釋放后重新拋出。這樣可以確保資源在發(fā)生異常的情況下也能被正確地釋放。在函數(shù)體內(nèi)部,除了常規(guī)邏輯可能拋出錯(cuò)誤外,Symbol.dispose 方法本身也可能會(huì)拋出錯(cuò)誤。如果在資源釋放期間發(fā)生了錯(cuò)誤,那么這個(gè)錯(cuò)誤也會(huì)被重新拋出。
但是,如果在清理之前和清理期間的邏輯都拋出錯(cuò)誤會(huì)怎樣呢?為了處理這些情況,引入了一個(gè)新的 Error 子類型,名為 SuppressedError。它具有一個(gè) suppressed 屬性,用于保存最后拋出的錯(cuò)誤,以及一個(gè) error 屬性,用于保存最近拋出的錯(cuò)誤。
當(dāng)在處理資源時(shí)發(fā)生多個(gè)錯(cuò)誤時(shí),SuppressedError 類型的錯(cuò)誤對(duì)象可以保留最新的錯(cuò)誤,并將之前發(fā)生的錯(cuò)誤標(biāo)記為被壓制的錯(cuò)誤。這種機(jī)制允許我們更好地跟蹤和處理多個(gè)錯(cuò)誤的情況。
假設(shè)在進(jìn)行資源清理時(shí),首先發(fā)生了一個(gè)錯(cuò)誤A,然后又發(fā)生了一個(gè)錯(cuò)誤B。在使用 SuppressedError 錯(cuò)誤對(duì)象時(shí),它會(huì)將錯(cuò)誤B作為最新錯(cuò)誤記錄,并將錯(cuò)誤A標(biāo)記為被壓制的錯(cuò)誤。這樣一來,我們可以通過 SuppressedError 對(duì)象獲取到兩個(gè)錯(cuò)誤的相關(guān)信息,從而更全面地了解發(fā)生的錯(cuò)誤情況。
class ErrorA extends Error {
name = "ErrorA";
}
class ErrorB extends Error {
name = "ErrorB";
}
function throwy(id: string) {
return {
[Symbol.dispose]() {
throw new ErrorA(`Error from ${id}`);
}
};
}
function func() {
using a = throwy("a");
throw new ErrorB("oops!")
}
try {
func();
}
catch (e: any) {
console.log(e.name); // SuppressedError
console.log(e.message); // An error was suppressed during disposal.
console.log(e.error.name); // ErrorA
console.log(e.error.message); // Error from a
console.log(e.suppressed.name); // ErrorB
console.log(e.suppressed.message); // oops!
}
可以看到,這些示例中都使用了同步方法。 但是,許多資源處置涉及到異步操作,我們需要等待這些操作完成才能繼續(xù)運(yùn)行其他代碼。
因此,還引入了一個(gè)名為 Symbol.asyncDispose 的新 symbol,并且?guī)砹艘粋€(gè)新特性:await using 聲明。它們與 using 聲明類似,但區(qū)別在于它們會(huì)查找需要等待其釋放的對(duì)象。它們使用由 Symbol.asyncDispose 命名的不同方法,盡管也可以操作具有 Symbol.dispose 的對(duì)象。為了方便起見,TypeScript 還引入了一個(gè)全局類型 AsyncDisposable,用于描述具有異步釋放方法的對(duì)象。
async function doWork() {
await new Promise(resolve => setTimeout(resolve, 500));
}
function loggy(id: string): AsyncDisposable {
console.log(`Constructing ${id}`);
return {
async [Symbol.asyncDispose]() {
console.log(`Disposing (async) ${id}`);
await doWork();
},
}
}
async function func() {
await using a = loggy("a");
await using b = loggy("b");
{
await using c = loggy("c");
await using d = loggy("d");
}
await using e = loggy("e");
return;
// 不會(huì)執(zhí)行
// 不會(huì)創(chuàng)建,不會(huì)處置
await using f = loggy("f");
}
func();
// Constructing a
// Constructing b
// Constructing c
// Constructing d
// Disposing (async) d
// Disposing (async) c
// Constructing e
// Disposing (async) e
// Disposing (async) b
// Disposing (async) a
使用 Disposable 和 AsyncDisposable 來定義類型可以使代碼更易于處理。實(shí)際上,許多已存在的類型都具有 dispose() 或 close() 方法,這些方法用于資源清理。例如,Visual Studio Code 的 API 甚至定義了它們自己的 Disposable 接口。瀏覽器和像 Node.js、Deno、Bun 這樣的運(yùn)行時(shí)中的 API 也可以選擇為已經(jīng)具有清理方法的對(duì)象使用 Symbol.dispose 和 Symbol.asyncDispose。
也許這對(duì)于庫來說聽起來很不錯(cuò),但對(duì)于一些場(chǎng)景來說可能有些過重。如果需要進(jìn)行大量的臨時(shí)清理工作,創(chuàng)建一個(gè)新的類型可能會(huì)引入過多的抽象。例如,再看一下上面 TempFile 的例子:
class TempFile implements Disposable {
#path: string;
#handle: number;
constructor(path: string) {
this.#path = path;
this.#handle = fs.openSync(path, "w+");
}
// 其他操作
[Symbol.dispose]() {
// 關(guān)閉文件并清理它
fs.closeSync(this.#handle);
fs.unlinkSync(this.#path);
}
}
export function doSomeWork() {
using file = new TempFile(".some_temp_file");
// 操作文件...
if (someCondition()) {
// 其他操作...
return;
}
}
我們只是想記住調(diào)用兩個(gè)函數(shù),但是這種寫法是最好的嗎?我們應(yīng)該在構(gòu)造函數(shù)中調(diào)用 openSync、創(chuàng)建一個(gè) open() 方法,還是自己傳入處理方法?應(yīng)該為每個(gè)可能的操作都暴露一個(gè)方法,還是將屬性設(shè)為 public?
這就引出了新特性的主角:DisposableStack 和 AsyncDisposableStack。這些對(duì)象非常適用于一次性的清理操作,以及任意數(shù)量的清理工作。DisposableStack 是一個(gè)對(duì)象,它具有多個(gè)用于跟蹤 Disposable 對(duì)象的方法,并且可以接收?qǐng)?zhí)行任意清理工作的函數(shù)。同時(shí),我們也可以將 DisposableStack 分配給 using 變量,這意味著我們可以將其用于資源管理,并在使用完成后自動(dòng)釋放資源。這是因?yàn)?nbsp;DisposableStack 本身也實(shí)現(xiàn)了 Disposable 接口,所以可以像使用其他 Disposable 對(duì)象一樣使用它。
下面是改寫原例子的方式:
function doSomeWork() {
const path = ".some_temp_file";
const file = fs.openSync(path, "w+");
using cleanup = new DisposableStack();
cleanup.defer(() => {
fs.closeSync(file);
fs.unlinkSync(path);
});
// 操作文件...
if (someCondition()) {
// 其他操作...
return;
}
// ...
}
這里的defer() 方法接受一個(gè)回調(diào)函數(shù),該回調(diào)函數(shù)將在清理被釋放時(shí)運(yùn)行。通常情況下,defer()(以及其他類似的 DisposableStack 方法,如 use 和 adopt)應(yīng)該在創(chuàng)建資源后立即調(diào)用。正如其名稱所示,DisposableStack 以棧的方式處理它所跟蹤的所有內(nèi)容,按照先進(jìn)后出的順序進(jìn)行清理,因此立即在創(chuàng)建值后延遲執(zhí)行可幫助避免奇怪的依賴問題。AsyncDisposable 的工作原理類似,但可以跟蹤異步函數(shù)和 AsyncDisposable,并且本身也是一個(gè) AsyncDisposable。
由于這個(gè)特性非常新,大多數(shù)運(yùn)行時(shí)環(huán)境不會(huì)原生支持它。要使用它,需要為以下內(nèi)容提供運(yùn)行時(shí)的 polyfills:
- Symbol.dispose
- Symbol.asyncDispose
- DisposableStack
- AsyncDisposableStack
- SuppressedError
然而,如果你只關(guān)注使用 using 和 await using,只需要提供內(nèi)置 symbol 的 polyfill 就足夠了。對(duì)于大多數(shù)情況,下面這樣簡(jiǎn)單的實(shí)現(xiàn)應(yīng)該可以工作:
Symbol.dispose ??= Symbol("Symbol.dispose");
Symbol.asyncDispose ??= Symbol("Symbol.asyncDispose");
此外,還需要將編譯目標(biāo)設(shè)置為 es2022 或更低,并在 lib 設(shè)置中配置 "esnext" 或 "esnext.disposable"。
{
"compilerOptions": {
"target": "es2022",
"lib": ["es2022", "esnext.disposable", "dom"]
}
}
裝飾器元數(shù)據(jù)
TypeScript 5.2 實(shí)現(xiàn)了一個(gè)即將推出的 ECMAScript 功能,稱為裝飾器元數(shù)據(jù)。這個(gè)功能的關(guān)鍵思想是讓裝飾器能夠在它們所用于或內(nèi)嵌的任何類上輕松創(chuàng)建和使用元數(shù)據(jù)。
無論何時(shí)使用裝飾器函數(shù),它們都可以在上下文對(duì)象的新metadata屬性上進(jìn)行訪問。metadata屬性僅包含一個(gè)簡(jiǎn)單的對(duì)象。由于JavaScript允許我們?nèi)我馓砑訉傩?,它可以被用作一個(gè)由每個(gè)裝飾器更新的字典。另外,由于每個(gè)裝飾部分的元數(shù)據(jù)對(duì)象將對(duì)于類的每個(gè)裝飾部分都是相同的,它可以作為一個(gè)Map的鍵。當(dāng)類上的所有裝飾器都執(zhí)行完畢后,可以通過Symbol.metadata從類上訪問該對(duì)象。
interface Context {
name: string;
metadata: Record<PropertyKey, unknown>;
}
function setMetadata(_target: any, context: Context) {
context.metadata[context.name] = true;
}
class SomeClass {
@setMetadata
foo = 123;
@setMetadata
accessor bar = "hello!";
@setMetadata
baz() { }
}
const ourMetadata = SomeClass[Symbol.metadata];
console.log(JSON.stringify(ourMetadata));
// { "bar": true, "baz": true, "foo": true }
這在許多不同的場(chǎng)景中都非常有用。元數(shù)據(jù)可以用于許多用途,例如調(diào)試、序列化或者在使用裝飾器進(jìn)行依賴注入時(shí)。由于每個(gè)被裝飾的類都會(huì)創(chuàng)建相應(yīng)的元數(shù)據(jù)對(duì)象,框架可以將它們作為私有的鍵入到 Map 或 WeakMap 中,或者根據(jù)需要添加屬性。
舉個(gè)例子,假設(shè)想要使用裝飾器來跟蹤哪些屬性和訪問器在使用JSON.stringify進(jìn)行序列化時(shí)是可序列化的,代碼示例如下:
import { serialize, jsonify } from "./serializer";
class Person {
firstName: string;
lastName: string;
@serialize
age: number
@serialize
get fullName() {
return `${this.firstName} ${this.lastName}`;
}
toJSON() {
return jsonify(this)
}
constructor(firstName: string, lastName: string, age: number) {
// ...
}
}
這里的意圖是只有 age 和 fullName 應(yīng)該被序列化,因?yàn)樗鼈儤?biāo)記了 @serialize 裝飾器。我們?yōu)榇硕x了一個(gè) toJSON 方法,但它只是調(diào)用了 jsonify,后者使用了 @serialize 創(chuàng)建的元數(shù)據(jù)。
const serializables = Symbol();
type Context =
| ClassAccessorDecoratorContext
| ClassGetterDecoratorContext
| ClassFieldDecoratorContext
;
export function serialize(_target: any, context: Context): void {
if (context.static || context.private) {
throw new Error("Can only serialize public instance members.")
}
if (typeof context.name === "symbol") {
throw new Error("Cannot serialize symbol-named properties.");
}
const propNames =
(context.metadata[serializables] as string[] | undefined) ??= [];
propNames.push(context.name);
}
export function jsonify(instance: object): string {
const metadata = instance.constructor[Symbol.metadata];
const propNames = metadata?.[serializables] as string[] | undefined;
if (!propNames) {
throw new Error("No members marked with @serialize.");
}
const pairStrings = propNames.map(key => {
const strKey = JSON.stringify(key);
const strValue = JSON.stringify((instance as any)[key]);
return `${strKey}: ${strValue}`;
});
return `{ ${pairStrings.join(", ")} }`;
}
這個(gè)模塊使用了一個(gè)稱為 serializables 的局部 symbol,用于存儲(chǔ)和檢索被標(biāo)記為 @serializable 的屬性名稱。它在每次調(diào)用 @serializable 時(shí)將這些屬性名稱存儲(chǔ)在元數(shù)據(jù)上。當(dāng)調(diào)用 jsonify 函數(shù)時(shí),會(huì)從元數(shù)據(jù)中獲取屬性列表,并使用這些列表從實(shí)例中檢索實(shí)際的屬性值,最終對(duì)這些名稱和值進(jìn)行序列化。
使用符號(hào)(Symbol)確實(shí)可以提供一定程度的私密性,因?yàn)樗鼈儾蝗菀妆煌獠吭L問到。然而,如果其他人知道了符號(hào)的存在,并且能夠獲取到對(duì)象的原型,他們?nèi)匀豢梢酝ㄟ^符號(hào)來訪問和修改元數(shù)據(jù)。因此,在這種情況下,符號(hào)并不能完全保證數(shù)據(jù)的私密性。
作為替代方案,可以使用 WeakMap 來存儲(chǔ)元數(shù)據(jù)。WeakMap 是一種特殊的 Map 數(shù)據(jù)結(jié)構(gòu),它的鍵只能是對(duì)象,并且不會(huì)阻止對(duì)象被垃圾回收。在這種情況下,我們可以使用每個(gè)對(duì)象的原型作為鍵,將元數(shù)據(jù)存儲(chǔ)在對(duì)應(yīng)的 WeakMap 實(shí)例中。這樣,只有持有 WeakMap 實(shí)例的代碼才能夠訪問和操作元數(shù)據(jù),確保了數(shù)據(jù)的私密性,也減少了在代碼中進(jìn)行類型斷言的次數(shù)。
const serializables = new WeakMap<object, string[]>();
type Context =
| ClassAccessorDecoratorContext
| ClassGetterDecoratorContext
| ClassFieldDecoratorContext
;
export function serialize(_target: any, context: Context): void {
if (context.static || context.private) {
throw new Error("Can only serialize public instance members.")
}
if (typeof context.name !== "string") {
throw new Error("Can only serialize string properties.");
}
let propNames = serializables.get(context.metadata);
if (propNames === undefined) {
serializables.set(context.metadata, propNames = []);
}
propNames.push(context.name);
}
export function jsonify(instance: object): string {
const metadata = instance.constructor[Symbol.metadata];
const propNames = metadata && serializables.get(metadata);
if (!propNames) {
throw new Error("No members marked with @serialize.");
}
const pairStrings = propNames.map(key => {
const strKey = JSON.stringify(key);
const strValue = JSON.stringify((instance as any)[key]);
return `${strKey}: ${strValue}`;
});
return `{ ${pairStrings.join(", ")} }`;
}
由于這個(gè)特性還比較新,大多數(shù)運(yùn)行環(huán)境尚未對(duì)其提供原生支持。如果要使用它,需要為 Symbol.metadata 添加一個(gè) polyfill。下面這個(gè)簡(jiǎn)單的示例應(yīng)該適用于大多數(shù)情況:
Symbol.metadata ??= Symbol("Symbol.metadata");
此外,還需要將編譯目標(biāo)設(shè)置為 es2022 或更低,并在 lib 設(shè)置中配置 "esnext" 或 "esnext.disposable"。
{
"compilerOptions": {
"target": "es2022",
"lib": ["es2022", "esnext.decorators", "dom"]
}
}
命名和匿名元組元素
元組類型支持為每個(gè)元素提供可選的標(biāo)記或名稱。
type Pair<T> = [first: T, second: T];
這些標(biāo)記并不會(huì)改變對(duì)元組的操作能力,它們僅僅是為了增加可讀性和工具支持。
然而,在以前的 TypeScript 版本中,有一個(gè)規(guī)則是元組不能在標(biāo)記和非標(biāo)記元素之間混合使用。換句話說,要么所有的元素都不帶標(biāo)記,要么所有的元素都需要帶標(biāo)記。
// ? 沒有標(biāo)記
type Pair1<T> = [T, T];
// ? 都有標(biāo)記
type Pair2<T> = [first: T, second: T];
// ?
type Pair3<T> = [first: T, T];
// ~
// Tuple members must all have names or all not have names.
對(duì)于剩余元素,這可能會(huì)變得有些麻煩,因?yàn)橹荒軓?qiáng)制添加一個(gè)標(biāo)簽,比如"rest"或"tail"。
type TwoOrMore_A<T> = [first: T, second: T, ...T[]];
// ~~~~~~
// Tuple members must all have names or all not have names.
// ?
type TwoOrMore_B<T> = [first: T, second: T, rest: ...T[]];
這也意味著這個(gè)限制必須在類型系統(tǒng)內(nèi)部進(jìn)行強(qiáng)制實(shí)施,這意味著 TypeScript 將丟失標(biāo)記。
正如之前提到的,為了確保在元組類型中所有元素要么都帶有標(biāo)簽,要么都不帶標(biāo)簽的規(guī)則,TypeScript 在類型系統(tǒng)內(nèi)部進(jìn)行了相應(yīng)的限制。這意味著 TypeScript 在類型檢查過程中會(huì)忽略元組元素的標(biāo)簽信息,對(duì)于類型系統(tǒng)而言,元組中的元素只被視為按照它們的順序排列的一組類型。
因此,盡管您在定義元組類型時(shí)可以使用標(biāo)簽,但在類型檢查和類型推斷過程中,TypeScript 不會(huì)考慮這些標(biāo)簽。只有元組元素的順序和類型才會(huì)被 TypeScript 確認(rèn)和驗(yàn)證。
這樣做是為了確保遵守 TypeScript 的語法規(guī)則并維持類型系統(tǒng)的一致性。盡管在類型定義中可能會(huì)丟失標(biāo)簽信息,但這不會(huì)影響元組的使用和功能。
type HasLabels = [a: string, b: string];
type HasNoLabels = [number, number];
type Merged = [...HasNoLabels, ...HasLabels];
// ^ [number, number, string, string]
//
// 'a' and 'b' were lost in 'Merged'
在 TypeScript 5.2 中,對(duì)元組標(biāo)記的全有或全無限制已經(jīng)被解除,可以更好地處理帶有標(biāo)簽的元組和展開操作?,F(xiàn)在,可以在定義元組時(shí)為每個(gè)元素指定一個(gè)標(biāo)記,并且在展開操作中保留這些標(biāo)記。
聯(lián)合類型數(shù)組方法調(diào)用
在之前的 TypeScript 版本中,數(shù)組聯(lián)合類型調(diào)用方法可能會(huì)導(dǎo)致一些問題。
declare let array: string[] | number[];
array.filter(x => !!x);
// ~~~~~~ error!
報(bào)錯(cuò)如下:
此表達(dá)式不可調(diào)用。
聯(lián)合類型 "{ <S extends string>(predicate: (value: string, index: number, array: string[]) => value is S, thisArg?: any): S[]; (predicate: (value: string, index: number, array: string[]) => unknown, thisArg?: any): string[]; } | { ...; }" 的每個(gè)成員都有簽名,但這些簽名都不能互相兼容。ts(2349)
在 TypeScript 5.2 中,對(duì)于數(shù)組聯(lián)合類型的方法調(diào)用進(jìn)行了特殊處理。在之前的版本中,TypeScript 會(huì)嘗試確定每個(gè)數(shù)組類型的方法是否在整個(gè)聯(lián)合類型上都兼容。然而,由于缺乏一種一致的策略,TypeScript 在這些情況下往往束手無策。
在 TypeScript 5.2 中,對(duì)于數(shù)組的聯(lián)合類型,會(huì)采取一種特殊的處理方式。首先,根據(jù)每個(gè)成員的元素類型構(gòu)造一個(gè)新的數(shù)組類型,然后在該類型上調(diào)用方法。
以上面的例子為例,string[] | number[] 被轉(zhuǎn)換為 (string | number)[](或者 Array<string | number>),然后在該類型上調(diào)用 filter 方法。需要注意的是,filter 方法將產(chǎn)生一個(gè) Array<string | number> 而不是 string[] | number[],但是針對(duì)一個(gè)全新生成的值,出錯(cuò)的風(fēng)險(xiǎn)較小。
這意味著在 TypeScript 5.2 中,許多像 filter、find、some、every 和 reduce 這樣的方法都可以在數(shù)組聯(lián)合類型上調(diào)用,而在之前的版本中則無法實(shí)現(xiàn)。
對(duì)象成員的逗號(hào)自動(dòng)補(bǔ)全
在向?qū)ο筇砑有聦傩詴r(shí),很容易忘記添加逗號(hào)。以前,如果忘記了逗號(hào)并請(qǐng)求自動(dòng)補(bǔ)全,TypeScript 會(huì)給出與此無關(guān)的糟糕的補(bǔ)全結(jié)果,容易令人困惑。
在 TypeScript 5.2 中,當(dāng)忘記逗號(hào)時(shí),它會(huì)優(yōu)雅地提供對(duì)象成員的補(bǔ)全建議。為了避免直接拋出語法錯(cuò)誤,它還會(huì)自動(dòng)插入缺失的逗號(hào)。
內(nèi)聯(lián)變量重構(gòu)
TypeScript 5.2 現(xiàn)在提供了一種重構(gòu)功能,可以將一個(gè)變量的內(nèi)容復(fù)制并內(nèi)聯(lián)到該變量在代碼中的所有使用位置。
通常情況下,如果要替換一個(gè)變量的使用點(diǎn),我們需要手動(dòng)復(fù)制變量的內(nèi)容并替換每個(gè)使用點(diǎn)。而通過這個(gè)重構(gòu)功能,TypeScript 可以自動(dòng)完成這個(gè)過程,將變量的值內(nèi)聯(lián)到其所有使用位置,從而簡(jiǎn)化了代碼的修改過程。
"內(nèi)聯(lián)變量"重構(gòu)操作會(huì)將變量的值直接替換到變量的所有使用位置,從而消除了中間的變量。然而,這種操作可能會(huì)改變代碼的執(zhí)行順序和邏輯,因?yàn)樵臼峭ㄟ^變量來保存并復(fù)用值的地方,現(xiàn)在變成了每次都重新計(jì)算并使用初始化器的值。
這可能會(huì)導(dǎo)致一些意想不到的問題,特別是如果初始化器有副作用(例如修改其他變量或調(diào)用函數(shù))時(shí)。因此,使用"內(nèi)聯(lián)變量"重構(gòu)時(shí)需要謹(jǐn)慎,并確保了解代碼中可能發(fā)生的變化和影響。
重大變更和正確性修復(fù)
lib.d.ts 更改
為 DOM 生成的類型可能對(duì)代碼庫產(chǎn)生影響。要了解更多信息,請(qǐng)參閱 TypeScript 5.2 中的 DOM 更新。
#labeledElementDeclarations 可能包含未定義的元素
為了支持混合使用帶標(biāo)簽和未帶標(biāo)簽的元素,TypeScript 的 API 發(fā)生了細(xì)微的變化。 TupleType 的 labelsElementDeclarations 屬性可能在元素未標(biāo)記的每個(gè)位置保持未定義。
interface TupleType {
- labeledElementDeclarations?: readonly (NamedTupleMember | ParameterDeclaration)[];
+ labeledElementDeclarations?: readonly (NamedTupleMember | ParameterDeclaration | undefined)[];
}
#module 和 #moduleResolution 必須在最近的 Node.js 設(shè)置下匹配
-module 和 --moduleResolution 選項(xiàng)均支持 node16 和 nodenext 設(shè)置。 這些實(shí)際上是“現(xiàn)代 Node.js”設(shè)置,應(yīng)該在任何最近的 Node.js 項(xiàng)目中使用。 當(dāng)這兩個(gè)選項(xiàng)在是否使用 Node.js 相關(guān)設(shè)置方面不一致時(shí),項(xiàng)目實(shí)際上會(huì)配置錯(cuò)誤。
在 TypeScript 5.2 中,當(dāng)對(duì) --module 和 --moduleResolution 選項(xiàng)之一使用 node16 或 nodenext 時(shí),TypeScript 現(xiàn)在要求另一個(gè)具有類似的 Node.js 相關(guān)設(shè)置。 如果設(shè)置不同,可能會(huì)收到類似以下錯(cuò)誤消息。
Option 'moduleResolution' must be set to 'NodeNext' (or left unspecified) when option 'module' is set to 'NodeNext'.
或者:
Option 'module' must be set to 'Node16' when option 'moduleResolution' is set to 'Node16'.
因此,例如 --module esnext --moduleResolution node16 將被拒絕,但最好單獨(dú)使用 --module nodenext 或 --module esnext --moduleResolution bundler。
合并符號(hào)的一致導(dǎo)出檢查
當(dāng)兩個(gè)聲明合并時(shí),它們必須在是否都導(dǎo)出的問題上達(dá)成一致。由于一個(gè)錯(cuò)誤,TypeScript 在環(huán)境語境中,例如聲明文件或 declare module 塊中,錯(cuò)過了特定的情況。例如,在下面的示例中,如果 replaceInFile 一次被聲明為導(dǎo)出函數(shù),另一次被聲明為未導(dǎo)出的命名空間,TypeScript 將不會(huì)發(fā)出錯(cuò)誤。
declare module 'replace-in-file' {
export function replaceInFile(config: unknown): Promise<unknown[]>;
export {};
namespace replaceInFile {
export function sync(config: unknown): unknown[];
}
}
在環(huán)境模塊中,添加export { ... }或類似的導(dǎo)出語法,比如export default ...,會(huì)隱式改變所有聲明是否自動(dòng)導(dǎo)出的行為。TypeScript 現(xiàn)在更一致地識(shí)別這些令人困惑的語義,并且會(huì)在所有的replaceInFile聲明上發(fā)出錯(cuò)誤,要求它們的修飾符必須保持一致。將會(huì)出現(xiàn)以下錯(cuò)誤提示:
Individual declarations in merged declaration 'replaceInFile' must be all exported or all local.
參考:https://devblogs.microsoft.com/typescript/announcing-typescript-5-2-rc/。