TypeScript 5.0 正式釋出!

語言: CN / TW / HK

theme: orange

2023 年 3 月 17 日,TypeScript 5.0 正式釋出!此版本帶來了許多新功能,旨在使 TypeScript 更小、更簡單、更快。TypeScript 5.0 實現了新的裝飾器標準、更好地支援 Node 和打構建工具中的 ESM 專案的功能、庫作者控制泛型推導的新方法、擴充套件了 JSDoc 功能、簡化了配置,並進行了許多其他改進。

可以通過以下 npm 命令開始使用 TypeScript 5.0: shell npm install -D typescript 以下是 TypeScript 5.0 的主要更新:

  • 全新裝飾器
  • const 型別引數
  • extends 支援多配置檔案
  • 所有列舉都是聯合列舉
  • --moduleResolutionbundler
  • 自定義解析標誌
  • --verbatimModuleSyntax
  • 支援 export type *
  • JSDoc 支援 @satisfies
  • JSDoc 支援 @overload
  • 編輯器中不區分大小寫的匯入排序
  • 完善 switch/case
  • 優化速度、記憶體和包大小
  • 其他重大更改和棄用

全新裝飾器

裝飾器是即將推出的 ECMAScript 特性,它允許我們以可重用的方式自定義類及其成員。

考慮以下程式碼: ```typescript class Person { name: string; constructor(name: string) { this.name = name; }

greet() {
    console.log(`Hello, my name is ${this.name}.`);
}

}

const p = new Person("Ray"); p.greet(); 這裡的 `greet` 方法很簡單,在實際中它內部可能會跟複雜,比如需要執行非同步邏輯,或者進行遞迴,亦或是有副作用等。那就可能需要使用 `console.log` 來除錯 `greet`:typescript class Person { name: string; constructor(name: string) { this.name = name; }

greet() {
    console.log("LOG: Entering method.");

    console.log(`Hello, my name is ${this.name}.`);

    console.log("LOG: Exiting method.")
}

} ``` 如果有一種方法可以為每種方法做到這一點,可能會很好。

這就是裝飾器的用武之地。我們可以編寫一個名為 loggedMethod 的函式,如下所示: ```typescript function loggedMethod(originalMethod: any, _context: any) {

function replacementMethod(this: any, ...args: any[]) {
    console.log("LOG: Entering method.")
    const result = originalMethod.call(this, ...args);
    console.log("LOG: Exiting method.")
    return result;
}

return replacementMethod;

} ```

這裡用了很多 any,可以暫時忽略,這樣可以讓例子儘可能得簡單。

這裡,loggedMethod 需要傳入一個引數(originalMethod) 並返回一個函式。執行過程如下:

  1. 列印:LOG: Entering method.
  2. 將 this 及其所有引數傳遞給原始方法
  3. 列印:LOG: Exiting method.
  4. 返回原始方法的執行結果

現在我們就可以使用 loggedMethod 來修飾 greet 方法: ```typescript class Person { name: string; constructor(name: string) { this.name = name; }

@loggedMethod
greet() {
    console.log(`Hello, my name is ${this.name}.`);
}

}

const p = new Person("Ray"); p.greet(); 輸出如下:typescript LOG: Entering method. Hello, my name is Ray. LOG: Exiting method. `` 這裡我們在greet上面使用了loggedMethod作為裝飾器——注意這裡的寫法:@loggedMethod。這樣,它會被原始方法和context物件呼叫。因為loggedMethod返回了一個新函式,該函式替換了greet` 的原始定義。

loggedMethod 的第二個引數被稱為“ context 物件”,它包含一些關於如何宣告裝飾方法的有用資訊——比如它是 #private 成員還是靜態成員,或者方法的名稱是什麼。 下面來重寫 loggedMethod 以利用它並打印出被修飾的方法的名稱。 ```typescript function loggedMethod(originalMethod: any, context: ClassMethodDecoratorContext) { const methodName = String(context.name);

function replacementMethod(this: any, ...args: any[]) {
    console.log(`LOG: Entering method '${methodName}'.`)
    const result = originalMethod.call(this, ...args);
    console.log(`LOG: Exiting method '${methodName}'.`)
    return result;
}

return replacementMethod;

} `` TypeScript 提供了一個名為ClassMethodDecoratorContext的型別,它對方法裝飾器採用的context物件進行建模。除了元資料之外,方法的context物件還有一個有用的函式:addInitializer`。 這是一種掛接到建構函式開頭的方法(如果使用靜態方法,則掛接到類本身的初始化)。

舉個例子,在JavaScript中,經常會寫如下的模式: ```typescript class Person { name: string; constructor(name: string) { this.name = name;

    this.greet = this.greet.bind(this);
}

greet() {
    console.log(`Hello, my name is ${this.name}.`);
}

} 或者,`greet`可以宣告為初始化為箭頭函式的屬性。typescript class Person { name: string; constructor(name: string) { this.name = name; }

greet = () => {
    console.log(`Hello, my name is ${this.name}.`);
};

} 編寫這段程式碼是為了確保在`greet`作為獨立函式呼叫或作為回撥函式傳遞時不會重新繫結。typescript const greet = new Person("Ray").greet;

greet(); 可以編寫一個裝飾器,使用`addInitializer`在建構函式中為我們呼叫 `bind`。typescript function bound(originalMethod: any, context: ClassMethodDecoratorContext) { const methodName = context.name; if (context.private) { throw new Error('bound' cannot decorate private properties like ${methodName as string}.); } context.addInitializer(function () { this[methodName] = this[methodName].bind(this); }); } `bound`不會返回任何內容,所以當它裝飾一個方法時,它會保留原來的方法。相反,它會在其他欄位初始化之前新增邏輯。typescript class Person { name: string; constructor(name: string) { this.name = name; }

@bound
@loggedMethod
greet() {
    console.log(`Hello, my name is ${this.name}.`);
}

}

const p = new Person("Ray"); const greet = p.greet;

greet(); `` 注意,我們使用了兩個裝飾器:@bound@loggedMethod。這些裝飾是以“相反的順序”執行的。也就是說,@loggedMethod修飾了原始方法greet@bound修飾了@loggedMethod`的結果。在這個例子中,這沒有關係——但如果裝飾器有副作用或期望某種順序,則可能有關係。

可以將這些裝飾器放在同一行: typescript @bound @loggedMethod greet() { console.log(`Hello, my name is ${this.name}.`); } 我們甚至可以建立返回裝飾器函式的函式。這使得我們可以對最終的裝飾器進行一些自定義。如果我們願意,我們可以讓loggedMethod返回一個裝飾器,並自定義它記錄訊息的方式。 ```typescript function loggedMethod(headMessage = "LOG:") { return function actualDecorator(originalMethod: any, context: ClassMethodDecoratorContext) { const methodName = String(context.name);

    function replacementMethod(this: any, ...args: any[]) {
        console.log(`${headMessage} Entering method '${methodName}'.`)
        const result = originalMethod.call(this, ...args);
        console.log(`${headMessage} Exiting method '${methodName}'.`)
        return result;
    }

    return replacementMethod;
}

} 如果這樣做,必須在使用`loggedMethod`作為裝飾器之前呼叫它。然後,可以傳入任何字串作為記錄到控制檯的訊息的字首。typescript class Person { name: string; constructor(name: string) { this.name = name; }

@loggedMethod("")
greet() {
    console.log(`Hello, my name is ${this.name}.`);
}

}

const p = new Person("Ray"); p.greet(); 輸出結果如下:javascript Entering method 'greet'. Hello, my name is Ray. Exiting method 'greet'. `` 裝飾器可不僅僅用於方法,還可以用於屬性/欄位、gettersetter`和自動訪問器。甚至類本身也可以裝飾成子類化和註冊。

上面的loggedMethodbound裝飾器示例寫的很簡單,並省略了大量關於型別的細節。實際上,編寫裝飾器可能相當複雜。例如,上面的loggedMethod型別良好的版本可能看起來像這樣: ```typescript function loggedMethod( target: (this: This, ...args: Args) => Return, context: ClassMethodDecoratorContext Return> ) { const methodName = String(context.name);

function replacementMethod(this: This, ...args: Args): Return {
    console.log(`LOG: Entering method '${methodName}'.`)
    const result = target.call(this, ...args);
    console.log(`LOG: Exiting method '${methodName}'.`)
    return result;
}

return replacementMethod;

} `` 我們必須使用thisArgsreturn`型別引數分別建模this、引數和原始方法的返回型別。

具體定義裝飾器函式的複雜程度取決於想要保證什麼。需要記住,裝飾器的使用次數將超過它們的編寫次數,所以型別良好的版本通常是更好的——但顯然與可讀性有一個權衡,所以請儘量保持簡單。

const 型別引數

當推斷一個物件的型別時,TypeScript通常會選擇一個通用型別。例如,在本例中,names 的推斷型別是string[]: ```typescript type HasNames = { readonly names: string[] }; function getNamesExactly(arg: T): T["names"] { return arg.names; }

// names 的推斷型別為 string[] const names = getNamesExactly({ names: ["Alice", "Bob", "Eve"]}); 通常這樣做的目的是實現突變。然而,根據`getnames`確切的作用以及它的使用方式,通常情況下需要更具體的型別。到目前為止,通常不得不在某些地方新增`const`,以實現所需的推斷:typescript // 我們想要的型別: readonly ["Alice", "Bob", "Eve"] // 我們得到的型別: string[] const names1 = getNamesExactly({ names: ["Alice", "Bob", "Eve"]});

// 得到想要的型別:readonly ["Alice", "Bob", "Eve"] const names2 = getNamesExactly({ names: ["Alice", "Bob", "Eve"]} as const); 這寫起來會很麻煩,也很容易忘記。在 TypeScript 5.0 中,可以在型別引數宣告中新增`const`修飾符,從而使類`const`推斷成為預設值:typescript type HasNames = { names: readonly string[] }; function getNamesExactly(arg: T): T["names"] { // ^^^^^ return arg.names; }

// 推斷型別:readonly ["Alice", "Bob", "Eve"] // 注意,這裡不需要再寫 as const const names = getNamesExactly({ names: ["Alice", "Bob", "Eve"] }); 注意,`const`修飾符並不排斥可變值,也不需要不可變約束。使用可變型別約束可能會得到意外的結果。例如:typescript declare function fnBad(args: T): void;

// T仍然是string[],因為readonly ["a", "b", "c"]不能賦值給string[] fnBad(["a", "b" ,"c"]); `` 這裡,T的推斷候選值是readonly ["a", "b", "c"],而readonly陣列不能用於需要可變陣列的地方。在這種情況下,推理回退到約束,陣列被視為string[]`,呼叫仍然成功進行。

更好的定義應該使用readonly string[]: ```typescript declare function fnGood(args: T): void;

// T 是 readonly ["a", "b", "c"] fnGood(["a", "b" ,"c"]); 同樣,要記住,`const`修飾符隻影響在呼叫中編寫的物件、陣列和基本型別表示式的推斷,所以不會(或不能)用`const`修飾的引數將看不到任何行為的變化:typescript declare function fnGood(args: T): void; const arr = ["a", "b" ,"c"];

// T 仍然是 string[],const 修飾符沒有作用 fnGood(arr); ```

extends 支援多配置檔案

當管理多個專案時,通常每個專案的 tsconfig.json 檔案都會繼承於基礎配置。這就是為什麼TypeScript支援extends欄位,用於從compilerOptions中複製欄位。 typescript // packages/front-end/src/tsconfig.json { "extends": "../../../tsconfig.base.json", "compilerOptions": { "outDir": "../lib", // ... } } 但是,在某些情況下,可能希望從多個配置檔案進行擴充套件。例如,想象一下使用一個TypeScript 基本配置檔案到 npm。如果想讓所有的專案也使用npm中@tsconfig/strictest包中的選項,那麼有一個簡單的解決方案:將tsconfig.base.json擴充套件到@tsconfig/strictesttypescript // tsconfig.base.json { "extends": "@tsconfig/strictest/tsconfig.json", "compilerOptions": { // ... } } 這在一定程度上是有效的。 如果有任何專案不想使用 @tsconfig/strictest,就必須手動禁用這些選項,或者建立一個不從 @tsconfig/strictest 擴充套件的單獨版本的 tsconfig.base.json

為了提供更多的靈活性,Typescript 5.0 允許extends欄位接收多個項。例如,在這個配置檔案中: typescript { "extends": ["a", "b", "c"], "compilerOptions": { // ... } } 這樣寫有點像直接擴充套件 c,其中 c 擴充套件 b,b 擴充套件 a。 如果任何欄位“衝突”,則後一個項生效。

所以在下面的例子中,strictNullChecksnoImplicitAny 都會在最終的 tsconfig.json 中啟用。 ```typescript // tsconfig1.json { "compilerOptions": { "strictNullChecks": true } }

// tsconfig2.json { "compilerOptions": { "noImplicitAny": true } }

// tsconfig.json { "extends": ["./tsconfig1.json", "./tsconfig2.json"], "files": ["./index.ts"] } 可以用下面的方式重寫最上面的例子:typescript // packages/front-end/src/tsconfig.json { "extends": ["@tsconfig/strictest/tsconfig.json", "../../../tsconfig.base.json"], "compilerOptions": { "outDir": "../lib", // ... } } ```

所有列舉都是聯合列舉

當 TypeScript 最初引入列舉時,它只不過是一組具有相同型別的數值常量: typescript enum E { Foo = 10, Bar = 20, } E.Foo 和 E.Bar 唯一的特別之處在於它們可以分配給任何期望型別 E 的東西。除此之外,它們只是數字。 ```typescript function takeValue(e: E) {}

takeValue(E.Foo); // ✅ takeValue(123); // ❌ 直到 TypeScript 2.0 引入了列舉字面量型別,它賦予每個列舉成員自己的型別,並將列舉本身轉換為每個成員型別的聯合。它還允許我們只引用列舉型別的一個子集,並縮小這些型別。typescript // Color就像是一個聯合:Red | Orange | Yellow | Green | Blue | Violet enum Color { Red, Orange, Yellow, Green, Blue, / Indigo /, Violet }

// 每個列舉成員都有自己的型別,可以引用 type PrimaryColor = Color.Red | Color.Green | Color.Blue;

function isPrimaryColor(c: Color): c is PrimaryColor { // 縮小字面量型別可以捕獲bug // TypeScript在這裡會報錯,因為 // 最終會比較 Color.Red 和 Color.Green。 // 本想使用||,但不小心寫了&& return c === Color.Red && c === Color.Green && c === Color.Blue; } 給每個列舉成員指定自己的型別有一個問題,即這些型別在某種程度上與成員的實際值相關聯。在某些情況下,這個值是不可能計算出來的——例如,列舉成員可以通過函式呼叫進行初始化。typescript enum E { Blah = Math.random() } ``` 每當TypeScript遇到這些問題時,它都會悄無聲息地退出並使用舊的列舉策略。這意味著要放棄並集和字面量型別的所有優點。

TypeScript 5.0 通過為每個計算成員建立唯一的型別,設法將所有列舉轉換為聯合列舉。這意味著現在可以縮小所有列舉的範圍,並將其成員作為型別引用。

--moduleResolution

TypeScript 4.7 為 --module--moduleResolution 設定引入了 node16 和 nodenext 選項。這些選項的目的是更好地模擬 Node.js 中 ECMAScript 模組的精確查詢規則; 然而,這種模式有許多其他工具沒有真正執行的限制。

例如,在 Node.js 的 ECMAScript 模組中,任何相對匯入都需要包含副檔名。 ```typescript // entry.mjs import * as utils from "./utils"; // ❌ - 需要包括副檔名。

import * as utils from "./utils.mjs"; // ✅ ``` 在Node.js和瀏覽器中這樣做是有原因的——它使檔案查詢更快,並且更適合原始檔案伺服器。但對於許多使用打包工具的開發人員來說,node16/nodenext 的設定很麻煩,因為打包工具沒有這些限制中的大部分。在某些方面,node解析模式更適合使用打包工具的人。

但在某些方面,原有的 node 解析模式已經過時了。 大多數現代打包工具在 Node.js 中使用 ECMAScript 模組和 CommonJS 查詢規則的融合。

為了模擬打包工具是如何工作的,TypeScript 5.0 引入了一個新策略:--moduleResolution bundler typescript { "compilerOptions": { "target": "esnext", "moduleResolution": "bundler" } } 如果正在使用現代打包工具,如 Vite、esbuild、swc、Webpack、Parcel 或其他實現混合查詢策略的打包工具,那麼新的 bundler 選項應該非常適合你。

另一方面,如果正在編寫一個打算在 npm 上釋出的庫,使用bundler選項可以隱藏不使用bundler的使用者可能出現的相容性問題。因此,在這些情況下,使用node16nodenext解析選項可能是更好的方法。

自定義解析標誌

JavaScript 工具現在可以模擬“混合”解析規則,就像上面描述的打包工具模式一樣。 由於工具的支援可能略有不同,TypeScript 5.0 提供了啟用或禁用一些功能的方法。

allowImportingTsExtensions

--allowImportingTsExtensions 允許 TypeScript 檔案使用特定於 TypeScript 的副檔名(如 .ts.mts.tsx)相互匯入。

僅當啟用 --noEmit--emitDeclarationOnly 時才允許使用此標誌,因為這些匯入路徑在執行時無法在 JavaScript 輸出檔案中解析。 這裡的期望是解析器(例如打包工具、執行時或其他工具)將使 .ts檔案之間的這些匯入正常工作。

resolvePackageJsonExports

--resolvePackageJsonExports 強制 TypeScript 在從 node_modules 中的包中讀取時查詢 package.json 檔案的 exports 欄位。

resolvePackageJsonImports

--resolvePackageJsonImports 強制 TypeScript 在從其祖先目錄包含 package.json 的檔案執行以 # 開頭的查詢時查詢 package.json 檔案的 imports 欄位。

--moduleResolutionnode16nodenextbundler 選項下,此選項預設為 true。

allowArbitraryExtensions

在 TypeScript 5.0 中,當匯入路徑以不是已知 JavaScript 或 TypeScript 副檔名的副檔名結尾時,編譯器將以 {file basename}.d.{extension} 的形式查詢該路徑的宣告檔案。例如,如果在打包專案中使用 CSS loader,可能希望為這些樣式表編寫(或生成)宣告檔案: css /* app.css */ .cookie-banner { display: none; } typescript // app.d.css.ts declare const css: { cookieBanner: string; }; export default css; ```typescript // App.tsx import styles from "./app.css";

styles.cookieBanner; // string `` 預設情況下,這個匯入將引發一個錯誤,讓你知道TypeScript不理解這個檔案型別,你的執行時可能不支援匯入它。但是,如果已經配置了執行時或打包工具來處理它,則可以使用新--allowArbitraryExtensions`編譯器選項來抑制錯誤。

注意,可以通過新增一個名為 app.css.d.ts 而不是 app.d.css.ts 的宣告檔案通常可以實現類似的效果。然而,這只是通過 Node 對 CommonJS 的 require 解析規則實現的。嚴格來說,前者被解釋為一個名為 app.css.js 的 JavaScript 檔案的宣告檔案。 因為相關檔案匯入需要在 Node 的 ESM 支援中包含副檔名,所以在我們的例子中,TypeScript 會在 --moduleResolution node16 或 nodenext 下的 ESM 檔案中出錯。

customConditions

--customConditions 獲取當 TypeScript 從 package.json 的 [exports] 或 (https://nodejs.org/api/packages.html#exports)) 或 imports 欄位解析時應該成功的附加的條件列表。這些條件將新增到解析器預設使用的現有條件中。

例如,當此欄位在 tsconfig.json 中設定為: typescript { "compilerOptions": { "target": "es2022", "moduleResolution": "bundler", "customConditions": ["my-condition"] } } 任何時候在 package.json 中引用 exports 或 imports 欄位時,TypeScript 都會考慮名為 my-condition 的條件。

因此,當從具有以下 package.json 的包中匯入時: typescript { // ... "exports": { ".": { "my-condition": "./foo.mjs", "node": "./bar.mjs", "import": "./baz.mjs", "require": "./biz.mjs" } } } TypeScript 將嘗試查詢與foo.mjs對應的檔案。這個欄位只有在 node16、nodenext 和--modulerresolution為 bundler 時才有效。

--verbatimModuleSyntax

預設情況下,TypeScript 會執行一些稱為匯入省略的操作。如果這樣寫: ```typescript import { Car } from "./car";

export function drive(car: Car) { // ... } TypeScript 檢測到只對型別使用匯入並完全刪除匯入。輸出 JavaScript 可能是這樣的:typescript export function drive(car) { // ... } `` 大多數時候這很好,因為如果Car不是從./car匯出的值,將得到一個執行時錯誤。但對於某些邊界情況,它確實增加了一層複雜性。例如,沒有像import "./car"這樣的語句,即完全放棄了import`,這實際上對有無副作用的模組產生影響。

TypeScript 的 JavaScript emit 策略也有另外幾層複雜性——省略匯入並不總是由如何使用 import 驅動的,它通常還會參考值的宣告方式。所以並不總是很清楚是否像下面這樣的程式碼: typescript export { Car } from "./car"; 如果 Car 是用類之類的東西宣告的,那麼它可以儲存在生成的 JavaScript 檔案中。 但是,如果 Car 僅宣告為類型別名或介面,則 JavaScript 檔案不應匯出 Car。

雖然 TypeScript 可能能夠根據來自跨檔案的資訊做出這些發出決策,但並非每個編譯器都可以。

imports 和 exports 的型別修飾符在這些情況下會有幫助。我們可以明確指定importexport僅用於型別分析,並且可以在JavaScript檔案中使用型別修飾符完全刪除。 ```typescript // 這條語句可以在JS輸出中完全刪除 import type * as car from "./car";

// 在JS輸出中可以刪除命名的import/export Car import { type Car } from "./car"; export { type Car } from "./car"; `` 型別修飾符本身並不是很有用——預設情況下,模組省略仍然會刪除匯入,並且沒有強制區分型別和普通匯入和匯出。 因此 TypeScript 有標誌--importsNotUsedAsValues以確保使用type修飾符,--preserveValueImports以防止某些模組省略行為,以及--isolatedModules` 以確保 TypeScript 程式碼適用於不同的編譯器。 不幸的是,很難理解這 3 個標誌的細節,並且仍然存在一些具有意外行為的邊界情況。

TypeScript 5.0 引入了一個名為 --verbatimModuleSyntax 的新選項來簡化這種情況。規則要簡單得多,任何沒有 type 修飾符的匯入或匯出都會被保留。任何使用 type 修飾符的內容都會被完全刪除。 ```typescript // 完全被刪除 import type { A } from "a";

// 重寫為 'import { b } from "bcd";' import { b, type c, type d } from "bcd";

// 重寫為 'import {} from "xyz";' import { type xyz } from "xyz"; `` 有了這個新選項,所見即所得。不過,當涉及到模組互操作時,這確實有一些影響。 在此標誌下,當設定或副檔名暗示不同的模組系統時,ECMAScript 匯入和匯出不會被重寫為require呼叫。相反,會得到一個錯誤。 如果需要生成使用requiremodule.exports的程式碼,則必須使用早於 ES2015 的 TypeScript 模組語法: ![image.png](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/225d0c01da624db281c78b3d7ecf0b60~tplv-k3u1fbpfcp-zoom-1.image)雖然這是一個限制,但它確實有助於使一些問題更加明顯。 例如,忘記在 --module node16 下的package.json中設定type` 欄位是很常見的。 因此,開發人員會在沒有意識到的情況下開始編寫 CommonJS 模組而不是 ES 模組,從而給出意外的查詢規則和 JavaScript 輸出。 這個新標誌確保有意使用正在使用的檔案型別,因為語法是有意不同的。

因為 --verbatimModuleSyntax 提供了比 --importsNotUsedAsValues--preserveValueImports 更一致的作用,所以這兩個現有標誌被棄用了。

支援 export type *

當 TypeScript 3.8 引入僅型別匯入時,新語法不允許在 export * from "module" 或 export * as ns from "module" 重新匯出時使用。 TypeScript 5.0 添加了對這兩種形式的支援: ```typescript // models/vehicles.ts export class Spaceship { // ... }

// models/index.ts export type * as vehicles from "./vehicles";

// main.ts import { vehicles } from "./models";

function takeASpaceship(s: vehicles.Spaceship) { // ✅ }

function makeASpaceship() { return new vehicles.Spaceship(); // ^^^^^^^^ // vehicles 不能用作值,因為它是使用“export type”匯出的。 } ```

JSDoc 支援 @satisfies

TypeScript 4.9 引入了 satisfies 操作符。它確保表示式的型別是相容的,而不影響型別本身。以下面的程式碼為例: ```typescript interface CompilerOptions { strict?: boolean; outDir?: string; // ... }

interface ConfigSettings { compilerOptions?: CompilerOptions; extends?: string | string[]; // ... }

let myConfigSettings = { compilerOptions: { strict: true, outDir: "../lib", // ... },

extends: [
    "@tsconfig/strictest/tsconfig.json",
    "../../../tsconfig.base.json"
],

} satisfies ConfigSettings; 這裡,TypeScript 知道 `myCompilerOptions.extends` 是用陣列宣告的,因為雖然 `satisfies` 驗證了物件的型別,但它並沒有直接將其更改為 `CompilerOptions` 而丟失資訊。所以如果想對映到 `extends` 上,是可以的。typescript declare function resolveConfig(configPath: string): CompilerOptions;

let inheritedConfigs = myConfigSettings.extends.map(resolveConfig); `` 這對 TypeScript 使用者很有幫助,但是很多人使用 TypeScript 來使用 JSDoc 註釋對 JavaScript 程式碼進行型別檢查。 這就是為什麼 TypeScript 5.0 支援一個名為@satisfies` 的新 JSDoc 標籤,它做的事情完全一樣。

/** @satisfies */ 可以捕獲型別不匹配: ```typescript // @ts-check

/* * @typedef CompilerOptions * @prop {boolean} [strict] * @prop {string} [outDir] /

/* * @satisfies {CompilerOptions} / let myCompilerOptions = { outdir: "../lib", // ~~~~~~ oops! we meant outDir }; 但它會保留表示式的原始型別,允許稍後在程式碼中更精確地使用值。typescript // @ts-check

/* * @typedef CompilerOptions * @prop {boolean} [strict] * @prop {string} [outDir] /

/* * @typedef ConfigSettings * @prop {CompilerOptions} [compilerOptions] * @prop {string | string[]} [extends] /

/* * @satisfies {ConfigSettings} / let myConfigSettings = { compilerOptions: { strict: true, outDir: "../lib", }, extends: [ "@tsconfig/strictest/tsconfig.json", "../../../tsconfig.base.json" ], };

let inheritedConfigs = myConfigSettings.extends.map(resolveConfig); `/** @satisfies */` 也可以內嵌在任何帶括號的表示式上。 可以這樣寫 `myCompilerOptions`:typescript let myConfigSettings = / @satisfies {ConfigSettings} */ ({ compilerOptions: { strict: true, outDir: "../lib", }, extends: [ "@tsconfig/strictest/tsconfig.json", "../../../tsconfig.base.json" ], }); 這可能在函式呼叫時更有意義:typescript compileCode(/ @satisfies {CompilerOptions} */ ({ // ... })); ```

JSDoc 支援 @overload

在 TypeScript 中,可以為函式指定過載。 過載提供了一種方式,用不同的引數呼叫一個函式,並返回不同的結果。它可以限制呼叫者實際使用函式的方式,並優化將返回的結果。 ```typescript // 過載: function printValue(str: string): void; function printValue(num: number, maxFractionDigits?: number): void;

// 實現: function printValue(value: string | number, maximumFractionDigits?: number) { if (typeof value === "number") { const formatter = Intl.NumberFormat("en-US", { maximumFractionDigits, }); value = formatter.format(value); }

console.log(value);

} ``` 這裡,printValue 將字串或數字作為第一個引數。如果它需要一個數字,它可以使用第二個引數來確定可以列印多少個小數位。

TypeScript 5.0 現在允許 JSDoc 使用新的 @overload 標籤宣告過載。 每個帶有 @overload標籤的 JSDoc 註釋都被視為以下函式宣告的不同過載。 ```typescript // @ts-check

/* * @overload * @param {string} value * @return {void} /

/* * @overload * @param {number} value * @param {number} [maximumFractionDigits] * @return {void} /

/* * @param {string | number} value * @param {number} [maximumFractionDigits] / function printValue(value, maximumFractionDigits) { if (typeof value === "number") { const formatter = Intl.NumberFormat("en-US", { maximumFractionDigits, }); value = formatter.format(value); }

console.log(value);

} 現在,無論是在 TypeScript 還是 JavaScript 檔案中編寫,TypeScript 都可以讓我們知道是否錯誤地呼叫了函式。typescript printValue("hello!"); printValue(123.45); printValue(123.45, 2);

printValue("hello!", 123); // ❌ ```

編輯器中不區分大小寫的匯入排序

在 Visual Studio 和 VS Code 等編輯器中,TypeScript 支援組織和排序匯入和匯出的體驗。 但是,對於列表何時“排序”,通常會有不同的解釋。

例如,下面的匯入列表是否排序? typescript import { Toggle, freeze, toBoolean, } from "./utils"; 答案可能是“視情況而定”。 如果不關心區分大小寫,那麼這個列表顯然沒有排序。 字母 f 出現在 t 和 T 之前。

但在大多數程式語言中,排序預設是比較字串的位元組值。JavaScript 比較字串的方式意味著“Toggle”總是在“freeze”之前,因為根據 ASCII 字元編碼,大寫字母在小寫字母之前。 所以從這個角度來看,匯入列表是已排序的。

TypeScript 之前認為匯入列表是已排序的,因為它會做基本的區分大小寫的排序。 對於喜歡不區分大小寫排序的開發人員,或者使用像 ESLint 這樣預設需要不區分大小寫排序的工具的開發人員來說,這可能是一個阻礙。

TypeScript 現在預設檢測大小寫。這意味著 TypeScript 和 ESLint 等工具通常不會就如何最好地對匯入進行排序而相互“鬥爭”。

這些選項最終可能由編輯器配置。目前,它們仍然不穩定且處於試驗階段,現在可以通過在 JSON 選項中使用 typescript.unstable 在 VS Code 中選擇加入它們。 以下是可以嘗試的所有選項(設定為預設值): ```typescript { "typescript.unstable": { // Should sorting be case-sensitive? Can be: // - true // - false // - "auto" (auto-detect) "organizeImportsIgnoreCase": "auto",

    // Should sorting be "ordinal" and use code points or consider Unicode rules? Can be:
    // - "ordinal"
    // - "unicode"
    "organizeImportsCollation": "ordinal",

    // Under `"organizeImportsCollation": "unicode"`,
    // what is the current locale? Can be:
    // - [any other locale code]
    // - "auto" (use the editor's locale)
    "organizeImportsLocale": "en",

    // Under `"organizeImportsCollation": "unicode"`,
    // should upper-case letters or lower-case letters come first? Can be:
    // - false (locale-specific)
    // - "upper"
    // - "lower"
    "organizeImportsCaseFirst": false,

    // Under `"organizeImportsCollation": "unicode"`,
    // do runs of numbers get compared numerically (i.e. "a1" < "a2" < "a100")? Can be:
    // - true
    // - false
    "organizeImportsNumericCollation": true,

    // Under `"organizeImportsCollation": "unicode"`,
    // do letters with accent marks/diacritics get sorted distinctly
    // from their "base" letter (i.e. is é different from e)? Can be
    // - true
    // - false
    "organizeImportsAccentCollation": true
},
"javascript.unstable": {
    // same options valid here...
},

} ```

完善 switch/case

在編寫 switch 語句時,TypeScript 現在會檢測被檢查的值何時具有字面量型別。以提供更便利的程式碼快捷輸入: switchCaseSnippets-5-0_1.gif

速度、記憶體和包大小優化

TypeScript 5.0 在程式碼結構、資料結構和演算法實現中包含許多強大的變化。這些都意味著整個體驗應該更快——不僅僅是執行 TypeScript,甚至安裝它。

以下是相對於 TypeScript 4.9 在速度和大小方面的優勢:

| 場景 | 時間或大小相對於 TS 4.9 | | --- | --- | | material-ui 構建時間 | 90% | | TypeScript 編譯器啟動時間 | 89% | | Playwright 構建時間 | 88% | | TypeScript 編譯器自構建時間 | 87% | | Outlook Web 構建時間 | 82% | | VS Code 構建時間 | 80% | | TypeScript npm 包大小 | 59% |

圖表形式: image.png TypeScript 包大小變化: image.png 那為什麼會有如此大的提升呢?部分優化細節如下:

首先,將 TypeScript 從名稱空間遷移到模組,這樣就能夠利用現代構建工具來執行優化。重新審視了打包策略並刪除一些已棄用的程式碼,已將 TypeScript 4.9 的 63.8 MB 包大小減少了約 26.4 MB。還通過直接函式呼叫帶來了顯著的速度提升。

在將資訊序列化為字串時,執行了一些快取。 型別顯示可能作為錯誤報告、宣告觸發、程式碼補全等的一部分發生,最終可能會相當昂貴。TypeScript 現在快取了一些常用的機制以在這些操作中重用。

總的來說,預計大多數程式碼庫應該會看到 TypeScript 5.0 的速度提升,並且始終能夠重現 10% 到 20% 之間的提升。當然,這將取決於硬體和程式碼庫特性。

其他重大更改和棄用

執行時要求

TypeScript 現在的 target 是 ECMAScript 2018。TypeScript 軟體包還將預期的最低引擎版本設定為 12.20。對於 Node.js 使用者來說,這意味著 TypeScript 5.0 需要至少Node.js 12.20 或更高版本才能執行。

lib.d.ts 變化

更改 DOM 型別的生成方式可能會對現有程式碼產生影響。注意,某些屬性已從數字轉換為數字字面量型別,並且用於剪下、複製和貼上事件處理的屬性和方法已跨介面移動。

API 重大變更

在 TypeScript 5.0 中, 轉向了模組,刪除了一些不必要的介面,並進行了一些正確性改進。

關係運算符中的禁止隱式強制

如果編寫的程式碼可能導致隱式字串到數字的強制轉換,TypeScript 中的某些操作現在會進行警告: typescript function func(ns: number | string) { return ns * 4; // 錯誤,可能存在隱式強制轉換 } 在 5.0 中,這也將應用於關係運算符 >、<、<= 和 >=: typescript function func(ns: number | string) { return ns > 4; } 如果需要這樣做,可以使用+顯式地將運算元轉換為數字: typescript function func(ns: number | string) { return +ns > 4; // OK }

棄用和預設更改

在 TypeScript 5.0 中,棄用了以下設定和設定值:

  • --target: ES3
  • --out
  • --noImplicitUseStrict
  • --keyofStringsOnly
  • --suppressExcessPropertyErrors
  • --suppressImplicitAnyIndexErrors
  • --noStrictGenericChecks
  • --charset
  • --importsNotUsedAsValues
  • --preserveValueImports

在 TypeScript 5.5 之前,這些配置將繼續被允許使用,屆時它們將被完全刪除,但是,如果正在使用這些設定,將收到警告。 在 TypeScript 5.0 以及未來版本 5.1、5.2、5.3 和 5.4 中,可以指定 "ignoreDeprecations": "5.0" 以消除這些警告。 很快會發佈一個 4.9 補丁,允許指定 ignoreDeprecations 以實現更平滑的升級。除了棄用之外,還更改了一些設定以更好地改進 TypeScript 中的跨平臺行為。

  • --newLine,控制 JavaScript 檔案中發出的行結束符,如果沒有指定,過去是根據當前作業系統推斷的。我們認為構建應該儘可能確定,Windows 記事本現在支援換行符,所以新的預設設定是 LF。 舊的特定於作業系統的推理行為不再可用。
  • --forceConsistentCasingInFileNames,它確保專案中對相同檔名的所有引用都在大小寫中達成一致,現在預設為 true。 這有助於捕獲在不區分大小寫的檔案系統上編寫的程式碼的差異問題。

參考資料