TypeScript 5.2 發布,支持顯式資源管理!
根據 TypeScript 路線圖,TypeScript 5.2 計劃于 8.22 發布。下面就來看看該版本都帶來了哪些新特性!
以下是 TypeScript 5.2 新增的功能:
- using 聲明和顯式資源管理
- 裝飾器元數據
- 命名和匿名元組元素
- 聯合類型數組方法調用
- 對象成員的逗號自動補全
- 內聯變量重構
- 重大變更和正確性修復
using 聲明和顯式資源管理
TypeScript 5.2 添加了對 ECMAScript 中即將推出的顯式資源管理功能的支持。
創建對象后通常需要進行某種“清理”。 例如,可能需要關閉網絡連接、刪除臨時文件或只是釋放一些內存空間。
假如有一個函數,它創建了一個臨時文件,通過各種操作讀寫該文件,然后關閉并刪除它。
import * as fs from "fs";
export function doSomeWork() {
const path = ".some_temp_file";
const file = fs.openSync(path, "w+");
// 操作文件...
// 關閉文件并刪除它
fs.closeSync(file);
fs.unlinkSync(path);
}
那如果需要提前退出怎么辦?
export function doSomeWork() {
const path = ".some_temp_file";
const file = fs.openSync(path, "w+");
// 操作文件...
if (someCondition()) {
// 其他操作...
// 關閉文件并刪除它
fs.closeSync(file);
fs.unlinkSync(path);
return;
}
// 關閉文件并刪除它
fs.closeSync(file);
fs.unlinkSync(path);
}
可以看到,這里就出現了重復的清理工作。如果拋出錯誤,我們也不保證關閉并刪除文件。這可以通過將這一切包裝在 try/finally 塊中來解決。
export function doSomeWork() {
const path = ".some_temp_file";
const file = fs.openSync(path, "w+");
try {
// 操作文件...
if (someCondition()) {
// 其他操作...
return;
}
}
finally {
// 關閉文件并刪除它
fs.closeSync(file);
fs.unlinkSync(path);
}
}
這樣寫雖然沒有什么大問題,但是會讓代碼變得復雜。如果我們向finally塊添加更多清理邏輯,還可能遇到其他問題,比如異常阻止了其他資源的釋放。 這就是顯式資源管理提案旨在解決的問題。該提案的關鍵思想是將資源釋放(要處理的清理工作)作為 JavaScript 中的一等公民來支持。
這一功能的實現方式是引入一個名為 Symbol.dispose 的新內置 symbol,并且可以創建具有以 Symbol.dispose 命名的方法的對象。為了方便起見,TypeScript 定義了一個名為 Disposable 的新全局類型,用于描述這些對象。
class TempFile implements Disposable {
#path: string;
#handle: number;
constructor(path: string) {
this.#path = path;
this.#handle = fs.openSync(path, "w+");
}
// 其他操作
[Symbol.dispose]() {
// 關閉文件并刪除它
fs.closeSync(this.#handle);
fs.unlinkSync(this.#path);
}
}
然后,可以調用這些方法:
export function doSomeWork() {
const file = new TempFile(".some_temp_file");
try {
// ...
}
finally {
file[Symbol.dispose]();
}
}
將清理邏輯移到 TempFile 本身并不能帶來很大的好處;基本上只是將所有的清理工作從 finally 塊中移到一個方法中,而這之前這就是可以實現的。但是,有一個眾所周知的方法名稱意味著 JavaScript 可以在其之上構建其他功能。
這就引出了該特性的第一個亮點:使用 using 聲明!using 是一個新的關鍵字,可以聲明新的固定綁定,有點類似于 const。關鍵的區別在于,使用 using 聲明的變量在作用域結束時會調用其 Symbol.dispose 方法!
所以我們可以簡單地編寫這段代碼:
export function doSomeWork() {
using file = new TempFile(".some_temp_file");
// 操作文件...
if (someCondition()) {
// 其他操作...
return;
}
}
可以看到,已經沒有了 try/finally 塊,從功能上來說,這就是 using 聲明為我們做的事情,但我們不必處理其中的細節。
如果你對 C# 的 using 聲明、Python 的 with 語句或者 Java 的 try-with-resource 聲明比較熟悉。就會發現,JavaScript 的新關鍵字 using 和它們類似,并提供了一種顯式的在作用域結束時執行對象的 "清理" 操作的方式。
**using**** 聲明會在其所屬的作用域的最后,或者在出現 "早期返回"(如 return 或拋出錯誤)之前進行清理操作。它們還按照先進后出的順序(類似于棧)進行釋放。
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;
// 不會執行
// 不會創建,不會處置
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 聲明的特點之一是在處理資源時具有異常處理的彈性。當使用聲明結束時,如果發生了錯誤,該錯誤將在資源釋放后重新拋出。這樣可以確保資源在發生異常的情況下也能被正確地釋放。在函數體內部,除了常規邏輯可能拋出錯誤外,Symbol.dispose 方法本身也可能會拋出錯誤。如果在資源釋放期間發生了錯誤,那么這個錯誤也會被重新拋出。
但是,如果在清理之前和清理期間的邏輯都拋出錯誤會怎樣呢?為了處理這些情況,引入了一個新的 Error 子類型,名為 SuppressedError。它具有一個 suppressed 屬性,用于保存最后拋出的錯誤,以及一個 error 屬性,用于保存最近拋出的錯誤。
當在處理資源時發生多個錯誤時,SuppressedError 類型的錯誤對象可以保留最新的錯誤,并將之前發生的錯誤標記為被壓制的錯誤。這種機制允許我們更好地跟蹤和處理多個錯誤的情況。
假設在進行資源清理時,首先發生了一個錯誤A,然后又發生了一個錯誤B。在使用 SuppressedError 錯誤對象時,它會將錯誤B作為最新錯誤記錄,并將錯誤A標記為被壓制的錯誤。這樣一來,我們可以通過 SuppressedError 對象獲取到兩個錯誤的相關信息,從而更全面地了解發生的錯誤情況。
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!
}
可以看到,這些示例中都使用了同步方法。 但是,許多資源處置涉及到異步操作,我們需要等待這些操作完成才能繼續運行其他代碼。
因此,還引入了一個名為 Symbol.asyncDispose 的新 symbol,并且帶來了一個新特性:await using 聲明。它們與 using 聲明類似,但區別在于它們會查找需要等待其釋放的對象。它們使用由 Symbol.asyncDispose 命名的不同方法,盡管也可以操作具有 Symbol.dispose 的對象。為了方便起見,TypeScript 還引入了一個全局類型 AsyncDisposable,用于描述具有異步釋放方法的對象。
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;
// 不會執行
// 不會創建,不會處置
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 來定義類型可以使代碼更易于處理。實際上,許多已存在的類型都具有 dispose() 或 close() 方法,這些方法用于資源清理。例如,Visual Studio Code 的 API 甚至定義了它們自己的 Disposable 接口。瀏覽器和像 Node.js、Deno、Bun 這樣的運行時中的 API 也可以選擇為已經具有清理方法的對象使用 Symbol.dispose 和 Symbol.asyncDispose。
也許這對于庫來說聽起來很不錯,但對于一些場景來說可能有些過重。如果需要進行大量的臨時清理工作,創建一個新的類型可能會引入過多的抽象。例如,再看一下上面 TempFile 的例子:
class TempFile implements Disposable {
#path: string;
#handle: number;
constructor(path: string) {
this.#path = path;
this.#handle = fs.openSync(path, "w+");
}
// 其他操作
[Symbol.dispose]() {
// 關閉文件并清理它
fs.closeSync(this.#handle);
fs.unlinkSync(this.#path);
}
}
export function doSomeWork() {
using file = new TempFile(".some_temp_file");
// 操作文件...
if (someCondition()) {
// 其他操作...
return;
}
}
我們只是想記住調用兩個函數,但是這種寫法是最好的嗎?我們應該在構造函數中調用 openSync、創建一個 open() 方法,還是自己傳入處理方法?應該為每個可能的操作都暴露一個方法,還是將屬性設為 public?
這就引出了新特性的主角:DisposableStack 和 AsyncDisposableStack。這些對象非常適用于一次性的清理操作,以及任意數量的清理工作。DisposableStack 是一個對象,它具有多個用于跟蹤 Disposable 對象的方法,并且可以接收執行任意清理工作的函數。同時,我們也可以將 DisposableStack 分配給 using 變量,這意味著我們可以將其用于資源管理,并在使用完成后自動釋放資源。這是因為 DisposableStack 本身也實現了 Disposable 接口,所以可以像使用其他 Disposable 對象一樣使用它。
下面是改寫原例子的方式:
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() 方法接受一個回調函數,該回調函數將在清理被釋放時運行。通常情況下,defer()(以及其他類似的 DisposableStack 方法,如 use 和 adopt)應該在創建資源后立即調用。正如其名稱所示,DisposableStack 以棧的方式處理它所跟蹤的所有內容,按照先進后出的順序進行清理,因此立即在創建值后延遲執行可幫助避免奇怪的依賴問題。AsyncDisposable 的工作原理類似,但可以跟蹤異步函數和 AsyncDisposable,并且本身也是一個 AsyncDisposable。
由于這個特性非常新,大多數運行時環境不會原生支持它。要使用它,需要為以下內容提供運行時的 polyfills:
- Symbol.dispose
- Symbol.asyncDispose
- DisposableStack
- AsyncDisposableStack
- SuppressedError
然而,如果你只關注使用 using 和 await using,只需要提供內置 symbol 的 polyfill 就足夠了。對于大多數情況,下面這樣簡單的實現應該可以工作:
Symbol.dispose ??= Symbol("Symbol.dispose");
Symbol.asyncDispose ??= Symbol("Symbol.asyncDispose");
此外,還需要將編譯目標設置為 es2022 或更低,并在 lib 設置中配置 "esnext" 或 "esnext.disposable"。
{
"compilerOptions": {
"target": "es2022",
"lib": ["es2022", "esnext.disposable", "dom"]
}
}
裝飾器元數據
TypeScript 5.2 實現了一個即將推出的 ECMAScript 功能,稱為裝飾器元數據。這個功能的關鍵思想是讓裝飾器能夠在它們所用于或內嵌的任何類上輕松創建和使用元數據。
無論何時使用裝飾器函數,它們都可以在上下文對象的新metadata屬性上進行訪問。metadata屬性僅包含一個簡單的對象。由于JavaScript允許我們任意添加屬性,它可以被用作一個由每個裝飾器更新的字典。另外,由于每個裝飾部分的元數據對象將對于類的每個裝飾部分都是相同的,它可以作為一個Map的鍵。當類上的所有裝飾器都執行完畢后,可以通過Symbol.metadata從類上訪問該對象。
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 }
這在許多不同的場景中都非常有用。元數據可以用于許多用途,例如調試、序列化或者在使用裝飾器進行依賴注入時。由于每個被裝飾的類都會創建相應的元數據對象,框架可以將它們作為私有的鍵入到 Map 或 WeakMap 中,或者根據需要添加屬性。
舉個例子,假設想要使用裝飾器來跟蹤哪些屬性和訪問器在使用JSON.stringify進行序列化時是可序列化的,代碼示例如下:
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 應該被序列化,因為它們標記了 @serialize 裝飾器。我們為此定義了一個 toJSON 方法,但它只是調用了 jsonify,后者使用了 @serialize 創建的元數據。
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(", ")} }`;
}
這個模塊使用了一個稱為 serializables 的局部 symbol,用于存儲和檢索被標記為 @serializable 的屬性名稱。它在每次調用 @serializable 時將這些屬性名稱存儲在元數據上。當調用 jsonify 函數時,會從元數據中獲取屬性列表,并使用這些列表從實例中檢索實際的屬性值,最終對這些名稱和值進行序列化。
使用符號(Symbol)確實可以提供一定程度的私密性,因為它們不容易被外部訪問到。然而,如果其他人知道了符號的存在,并且能夠獲取到對象的原型,他們仍然可以通過符號來訪問和修改元數據。因此,在這種情況下,符號并不能完全保證數據的私密性。
作為替代方案,可以使用 WeakMap 來存儲元數據。WeakMap 是一種特殊的 Map 數據結構,它的鍵只能是對象,并且不會阻止對象被垃圾回收。在這種情況下,我們可以使用每個對象的原型作為鍵,將元數據存儲在對應的 WeakMap 實例中。這樣,只有持有 WeakMap 實例的代碼才能夠訪問和操作元數據,確保了數據的私密性,也減少了在代碼中進行類型斷言的次數。
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(", ")} }`;
}
由于這個特性還比較新,大多數運行環境尚未對其提供原生支持。如果要使用它,需要為 Symbol.metadata 添加一個 polyfill。下面這個簡單的示例應該適用于大多數情況:
Symbol.metadata ??= Symbol("Symbol.metadata");
此外,還需要將編譯目標設置為 es2022 或更低,并在 lib 設置中配置 "esnext" 或 "esnext.disposable"。
{
"compilerOptions": {
"target": "es2022",
"lib": ["es2022", "esnext.decorators", "dom"]
}
}
命名和匿名元組元素
元組類型支持為每個元素提供可選的標記或名稱。
type Pair<T> = [first: T, second: T];
這些標記并不會改變對元組的操作能力,它們僅僅是為了增加可讀性和工具支持。
然而,在以前的 TypeScript 版本中,有一個規則是元組不能在標記和非標記元素之間混合使用。換句話說,要么所有的元素都不帶標記,要么所有的元素都需要帶標記。
// ? 沒有標記
type Pair1<T> = [T, T];
// ? 都有標記
type Pair2<T> = [first: T, second: T];
// ?
type Pair3<T> = [first: T, T];
// ~
// Tuple members must all have names or all not have names.
對于剩余元素,這可能會變得有些麻煩,因為只能強制添加一個標簽,比如"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[]];
這也意味著這個限制必須在類型系統內部進行強制實施,這意味著 TypeScript 將丟失標記。
正如之前提到的,為了確保在元組類型中所有元素要么都帶有標簽,要么都不帶標簽的規則,TypeScript 在類型系統內部進行了相應的限制。這意味著 TypeScript 在類型檢查過程中會忽略元組元素的標簽信息,對于類型系統而言,元組中的元素只被視為按照它們的順序排列的一組類型。
因此,盡管您在定義元組類型時可以使用標簽,但在類型檢查和類型推斷過程中,TypeScript 不會考慮這些標簽。只有元組元素的順序和類型才會被 TypeScript 確認和驗證。
這樣做是為了確保遵守 TypeScript 的語法規則并維持類型系統的一致性。盡管在類型定義中可能會丟失標簽信息,但這不會影響元組的使用和功能。
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 中,對元組標記的全有或全無限制已經被解除,可以更好地處理帶有標簽的元組和展開操作。現在,可以在定義元組時為每個元素指定一個標記,并且在展開操作中保留這些標記。
聯合類型數組方法調用
在之前的 TypeScript 版本中,數組聯合類型調用方法可能會導致一些問題。
declare let array: string[] | number[];
array.filter(x => !!x);
// ~~~~~~ error!
報錯如下:
此表達式不可調用。
聯合類型 "{ <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[]; } | { ...; }" 的每個成員都有簽名,但這些簽名都不能互相兼容。ts(2349)
在 TypeScript 5.2 中,對于數組聯合類型的方法調用進行了特殊處理。在之前的版本中,TypeScript 會嘗試確定每個數組類型的方法是否在整個聯合類型上都兼容。然而,由于缺乏一種一致的策略,TypeScript 在這些情況下往往束手無策。
在 TypeScript 5.2 中,對于數組的聯合類型,會采取一種特殊的處理方式。首先,根據每個成員的元素類型構造一個新的數組類型,然后在該類型上調用方法。
以上面的例子為例,string[] | number[] 被轉換為 (string | number)[](或者 Array<string | number>),然后在該類型上調用 filter 方法。需要注意的是,filter 方法將產生一個 Array<string | number> 而不是 string[] | number[],但是針對一個全新生成的值,出錯的風險較小。
這意味著在 TypeScript 5.2 中,許多像 filter、find、some、every 和 reduce 這樣的方法都可以在數組聯合類型上調用,而在之前的版本中則無法實現。
對象成員的逗號自動補全
在向對象添加新屬性時,很容易忘記添加逗號。以前,如果忘記了逗號并請求自動補全,TypeScript 會給出與此無關的糟糕的補全結果,容易令人困惑。
在 TypeScript 5.2 中,當忘記逗號時,它會優雅地提供對象成員的補全建議。為了避免直接拋出語法錯誤,它還會自動插入缺失的逗號。
內聯變量重構
TypeScript 5.2 現在提供了一種重構功能,可以將一個變量的內容復制并內聯到該變量在代碼中的所有使用位置。
通常情況下,如果要替換一個變量的使用點,我們需要手動復制變量的內容并替換每個使用點。而通過這個重構功能,TypeScript 可以自動完成這個過程,將變量的值內聯到其所有使用位置,從而簡化了代碼的修改過程。
"內聯變量"重構操作會將變量的值直接替換到變量的所有使用位置,從而消除了中間的變量。然而,這種操作可能會改變代碼的執行順序和邏輯,因為原本是通過變量來保存并復用值的地方,現在變成了每次都重新計算并使用初始化器的值。
這可能會導致一些意想不到的問題,特別是如果初始化器有副作用(例如修改其他變量或調用函數)時。因此,使用"內聯變量"重構時需要謹慎,并確保了解代碼中可能發生的變化和影響。
重大變更和正確性修復
lib.d.ts 更改
為 DOM 生成的類型可能對代碼庫產生影響。要了解更多信息,請參閱 TypeScript 5.2 中的 DOM 更新。
#labeledElementDeclarations 可能包含未定義的元素
為了支持混合使用帶標簽和未帶標簽的元素,TypeScript 的 API 發生了細微的變化。 TupleType 的 labelsElementDeclarations 屬性可能在元素未標記的每個位置保持未定義。
interface TupleType {
- labeledElementDeclarations?: readonly (NamedTupleMember | ParameterDeclaration)[];
+ labeledElementDeclarations?: readonly (NamedTupleMember | ParameterDeclaration | undefined)[];
}
#module 和 #moduleResolution 必須在最近的 Node.js 設置下匹配
-module 和 --moduleResolution 選項均支持 node16 和 nodenext 設置。 這些實際上是“現代 Node.js”設置,應該在任何最近的 Node.js 項目中使用。 當這兩個選項在是否使用 Node.js 相關設置方面不一致時,項目實際上會配置錯誤。
在 TypeScript 5.2 中,當對 --module 和 --moduleResolution 選項之一使用 node16 或 nodenext 時,TypeScript 現在要求另一個具有類似的 Node.js 相關設置。 如果設置不同,可能會收到類似以下錯誤消息。
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 將被拒絕,但最好單獨使用 --module nodenext 或 --module esnext --moduleResolution bundler。
合并符號的一致導出檢查
當兩個聲明合并時,它們必須在是否都導出的問題上達成一致。由于一個錯誤,TypeScript 在環境語境中,例如聲明文件或 declare module 塊中,錯過了特定的情況。例如,在下面的示例中,如果 replaceInFile 一次被聲明為導出函數,另一次被聲明為未導出的命名空間,TypeScript 將不會發出錯誤。
declare module 'replace-in-file' {
export function replaceInFile(config: unknown): Promise<unknown[]>;
export {};
namespace replaceInFile {
export function sync(config: unknown): unknown[];
}
}
在環境模塊中,添加export { ... }或類似的導出語法,比如export default ...,會隱式改變所有聲明是否自動導出的行為。TypeScript 現在更一致地識別這些令人困惑的語義,并且會在所有的replaceInFile聲明上發出錯誤,要求它們的修飾符必須保持一致。將會出現以下錯誤提示:
Individual declarations in merged declaration 'replaceInFile' must be all exported or all local.
參考:https://devblogs.microsoft.com/typescript/announcing-typescript-5-2-rc/。