裝飾器
裝飾器是一項即將推出的 ECMAScript 特性,它允許我們以可重用的方式自定義類及其成員。
讓我們看下面的程式碼
tsclass 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。
tsclass 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 的函式,如下所示
tsfunction 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 是怎麼回事?這是 anyScript 嗎!?”
請耐心一點——我們現在保持簡單,以便專注於這個函式正在做什麼。請注意,loggedMethod 接收原始方法 (originalMethod) 並返回一個函式,該函式:
- 記錄一條“Entering…”(進入)訊息
- 將
this及其所有引數傳遞給原始方法 - 記錄一條“Exiting…”(退出)訊息,並且
- 返回原始方法的返回值。
現在我們可以使用 loggedMethod 來裝飾方法 greet
tsclass Person {name: string;constructor(name: string) {this.name = name;}@loggedMethodgreet() {console.log(`Hello, my name is ${this.name}.`);}}const p = new Person("Ray");p.greet();// Output://// LOG: Entering method.// Hello, my name is Ray.// LOG: Exiting method.
我們剛剛在 greet 上方使用了 loggedMethod 作為裝飾器——注意我們將其寫為 @loggedMethod。當我們這樣做時,它會被呼叫,並傳入方法目標和一個上下文物件。因為 loggedMethod 返回了一個新函式,所以該函式替換了 greet 的原始定義。
我們之前沒有提到,但 loggedMethod 定義了第二個引數。它被稱為“上下文物件”,它包含了一些關於被裝飾方法如何宣告的有用資訊——比如它是否是一個 #private(私有)成員、static(靜態)成員,或者方法的名稱是什麼。讓我們重寫 loggedMethod 來利用這一點並打印出被裝飾方法的名稱。
tsfunction 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;}
我們現在正在使用上下文引數——這是 loggedMethod 中第一個型別比 any 和 any[] 更嚴格的內容。TypeScript 提供了一個名為 ClassMethodDecoratorContext 的型別,用於建模方法裝飾器所接收的上下文物件。
除了元資料之外,方法的上下文物件還有一個非常有用的函式叫 addInitializer。它是一種在建構函式開始時(如果我們處理的是 static,則是類本身初始化時)掛載邏輯的方法。
舉個例子——在 JavaScript 中,通常會編寫如下模式
tsclass 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 可能被宣告為一個初始化為箭頭函式的屬性。
tsclass Person {name: string;constructor(name: string) {this.name = name;}greet = () => {console.log(`Hello, my name is ${this.name}.`);};}
這段程式碼的編寫是為了確保如果 greet 作為獨立函式呼叫或作為回撥傳遞時,this 不會重新繫結。
tsconst greet = new Person("Ray").greet;// We don't want this to fail!greet();
我們可以編寫一個裝飾器,利用 addInitializer 為我們自動在建構函式中呼叫 bind。
tsfunction 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 沒有返回任何內容——所以當它裝飾一個方法時,它不會改變原始方法。相反,它會在任何其他欄位初始化之前新增邏輯。
tsclass Person {name: string;constructor(name: string) {this.name = name;}@bound@loggedMethodgreet() {console.log(`Hello, my name is ${this.name}.`);}}const p = new Person("Ray");const greet = p.greet;// Works!greet();
注意我們堆疊了兩個裝飾器——@bound 和 @loggedMethod。這些裝飾器以“相反的順序”執行。也就是說,@loggedMethod 裝飾原始方法 greet,而 @bound 裝飾 @loggedMethod 的結果。在這個例子中,順序無關緊要——但如果你的裝飾器有副作用或預期特定的順序,那麼順序就很重要了。
同樣值得注意的是,如果你在風格上更喜歡,可以將這些裝飾器放在同一行。
ts@bound @loggedMethod greet() {console.log(`Hello, my name is ${this.name}.`);}
可能不明顯的一點是,我們甚至可以建立返回裝飾器函式的函式。這使得自定義最終裝飾器成為可能。如果我們願意,可以讓 loggedMethod 返回一個裝飾器,並自定義它記錄訊息的方式。
tsfunction 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。然後我們可以傳入任何字串作為記錄到控制檯的訊息字首。
tsclass 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();// Output://// ⚠️ Entering method 'greet'.// Hello, my name is Ray.// ⚠️ Exiting method 'greet'.
裝飾器不僅可以用在方法上!它們還可以用在屬性/欄位、getter、setter 和自動訪問器 (auto-accessors) 上。甚至類本身也可以被裝飾,用於子類化和註冊等用途。
要深入瞭解裝飾器,可以閱讀 Axel Rauschmayer 的詳盡總結。
關於所涉及更改的更多資訊,你可以 檢視原始拉取請求。
與實驗性舊版裝飾器的差異
如果你使用 TypeScript 一段時間了,你可能知道它多年來一直支援“實驗性”裝飾器。雖然這些實驗性裝飾器非常有用,但它們模擬的是裝飾器提案的一個非常老的版本,並且總是需要一個名為 --experimentalDecorators 的編譯器標誌。任何在沒有此標誌的情況下在 TypeScript 中使用裝飾器的嘗試,過去都會觸發錯誤訊息。
--experimentalDecorators 在可預見的未來將繼續存在;然而,如果沒有該標誌,裝飾器現在將成為所有新程式碼的有效語法。除了 --experimentalDecorators 之外,它們的型別檢查和編譯輸出 (emit) 方式將會不同。型別檢查規則和編譯輸出差異很大,因此雖然裝飾器可以編寫為同時支援新舊裝飾器行為,但現有的裝飾器函式不太可能做到這一點。
這個新的裝飾器提案與 --emitDecoratorMetadata 不相容,並且不允許裝飾引數。未來的 ECMAScript 提案或許能夠填補這一空白。
最後一點:除了允許裝飾器放置在 export 關鍵字之前,裝飾器提案現在提供了將裝飾器放置在 export 或 export default 之後的選項。唯一的例外是禁止混合使用這兩種樣式。
js// ✅ allowed@register export default class Foo {// ...}// ✅ also allowedexport default @register class Bar {// ...}// ❌ error - before *and* after is not allowed@before export @after class Bar {// ...}
編寫型別良好的裝飾器
上面示例中的 loggedMethod 和 bound 裝飾器是有意簡化,並省略了許多關於型別的細節。
編寫裝飾器的型別可能相當複雜。例如,上面 loggedMethod 的型別良好版本可能看起來像這樣
tsfunction loggedMethod<This, Args extends any[], Return>(target: (this: This, ...args: Args) => Return,context: ClassMethodDecoratorContext<This, (this: This, ...args: Args) => 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;}
我們必須分別使用型別引數 This、Args 和 Return 來建模原始方法的 this 型別、引數和返回型別。
你的裝飾器函式定義得有多複雜,取決於你想要保證什麼。請記住,你的裝飾器被使用的次數遠多於編寫的次數,因此型別良好的版本通常是首選——但這顯然與可讀性之間存在權衡,所以儘量保持簡單。
關於編寫裝飾器的更多文件將在未來提供——但這篇文章應該已經包含了關於裝飾器機制的詳細資訊。
const 型別引數
當推斷物件的型別時,TypeScript 通常會選擇一個通用的型別。例如,在這種情況下,names 的推斷型別是 string[]
tstype HasNames = { names: readonly string[] };function getNamesExactly<T extends HasNames>(arg: T): T["names"] {return arg.names;}// Inferred type: string[]const names = getNamesExactly({ names: ["Alice", "Bob", "Eve"]});
通常這樣做的目的是為了以後能夠進行修改。
然而,根據 getNamesExactly 具體做了什麼以及它打算如何使用,通常需要一個更具體的型別。
到目前為止,API 作者通常必須建議在某些地方新增 as const 以達到所需的推斷
ts// The type we wanted:// readonly ["Alice", "Bob", "Eve"]// The type we got:// string[]const names1 = getNamesExactly({ names: ["Alice", "Bob", "Eve"]});// Correctly gets what we wanted:// readonly ["Alice", "Bob", "Eve"]const names2 = getNamesExactly({ names: ["Alice", "Bob", "Eve"]} as const);
這可能很麻煩且容易被遺忘。在 TypeScript 5.0 中,你現在可以向型別引數宣告新增 const 修飾符,從而使 const 風格的推斷成為預設值
tstype HasNames = { names: readonly string[] };function getNamesExactly<const T extends HasNames>(arg: T): T["names"] {// ^^^^^return arg.names;}// Inferred type: readonly ["Alice", "Bob", "Eve"]// Note: Didn't need to write 'as const' hereconst names = getNamesExactly({ names: ["Alice", "Bob", "Eve"] });
請注意,const 修飾符不會拒絕可變值,也不需要不可變約束。使用可變型別約束可能會產生令人驚訝的結果。例如
tsdeclare function fnBad<const T extends string[]>(args: T): void;// 'T' is still 'string[]' since 'readonly ["a", "b", "c"]' is not assignable to 'string[]'fnBad(["a", "b" ,"c"]);
這裡,T 的推斷候選是 readonly ["a", "b", "c"],而 readonly 陣列不能用於需要可變陣列的地方。在這種情況下,推斷會回退到約束,陣列被視為 string[],呼叫仍然可以成功進行。
該函式的更好定義應該使用 readonly string[]
tsdeclare function fnGood<const T extends readonly string[]>(args: T): void;// T is readonly ["a", "b", "c"]fnGood(["a", "b" ,"c"]);
同樣,請記住 const 修飾符僅影響在呼叫中編寫的物件、陣列和原始表示式的推斷,因此對於那些不會(或無法)使用 as const 修改的引數,行為不會有任何改變
tsdeclare function fnGood<const T extends readonly string[]>(args: T): void;const arr = ["a", "b" ,"c"];// 'T' is still 'string[]'-- the 'const' modifier has no effect herefnGood(arr);
檢視拉取請求以及(第一個和第二個)激勵問題以獲取更多詳細資訊。
支援在 extends 中使用多個配置檔案
在管理多個專案時,擁有一個其他 tsconfig.json 檔案可以從中繼承的“基礎”配置檔案會很有幫助。這就是 TypeScript 支援 extends 欄位以從 compilerOptions 複製欄位的原因。
jsonc// packages/front-end/src/tsconfig.json{"extends": "../../../tsconfig.base.json","compilerOptions": {"outDir": "../lib",// ...}}
但是,有些場景下你可能希望從多個配置檔案中繼承。例如,想象一下使用釋出到 npm 的 TypeScript 基礎配置檔案。如果你希望所有專案也使用 npm 上 @tsconfig/strictest 包中的選項,那麼有一個簡單的解決方案:讓 tsconfig.base.json 繼承 @tsconfig/strictest
jsonc// tsconfig.base.json{"extends": "@tsconfig/strictest/tsconfig.json","compilerOptions": {// ...}}
這在一定程度上有效。如果你的任何專案不想使用 @tsconfig/strictest,它們要麼必須手動停用這些選項,要麼建立一個不繼承 @tsconfig/strictest 的 tsconfig.base.json 的單獨版本。
為了提供更大的靈活性,TypeScript 5.0 現在允許 extends 欄位接收多個條目。例如,在這個配置檔案中
jsonc{"extends": ["a", "b", "c"],"compilerOptions": {// ...}}
這樣寫有點類似於直接繼承 c,其中 c 繼承 b,而 b 繼承 a。如果任何欄位“衝突”,則後面的條目獲勝。
所以在下面的示例中,最終的 tsconfig.json 中同時啟用了 strictNullChecks 和 noImplicitAny。
jsonc// tsconfig1.json{"compilerOptions": {"strictNullChecks": true}}// tsconfig2.json{"compilerOptions": {"noImplicitAny": true}}// tsconfig.json{"extends": ["./tsconfig1.json", "./tsconfig2.json"],"files": ["./index.ts"]}
作為另一個例子,我們可以用以下方式重寫我們最初的示例。
jsonc// packages/front-end/src/tsconfig.json{"extends": ["@tsconfig/strictest/tsconfig.json", "../../../tsconfig.base.json"],"compilerOptions": {"outDir": "../lib",// ...}}
有關更多詳細資訊,請閱讀原始拉取請求。
所有 enum(列舉)都是聯合列舉
當 TypeScript 最初引入列舉時,它們只不過是一組具有相同型別的數值常量。
tsenum E {Foo = 10,Bar = 20,}
E.Foo 和 E.Bar 的唯一特殊之處在於它們可以賦值給任何期望 E 型別的地方。除此之外,它們基本上只是 number(數字)。
tsfunction takeValue(e: E) {}takeValue(E.Foo); // workstakeValue(123); // error!
直到 TypeScript 2.0 引入列舉字面量型別,列舉才變得稍微特別一些。列舉字面量型別給了每個列舉成員它自己的型別,並將列舉本身變成了每個成員型別的聯合。它們還允許我們僅引用列舉型別的一個子集,並縮減這些型別。
ts// Color is like a union of Red | Orange | Yellow | Green | Blue | Violetenum Color {Red, Orange, Yellow, Green, Blue, /* Indigo, */ Violet}// Each enum member has its own type that we can refer to!type PrimaryColor = Color.Red | Color.Green | Color.Blue;function isPrimaryColor(c: Color): c is PrimaryColor {// Narrowing literal types can catch bugs.// TypeScript will error here because// we'll end up comparing 'Color.Red' to 'Color.Green'.// We meant to use ||, but accidentally wrote &&.return c === Color.Red && c === Color.Green && c === Color.Blue;}
給每個列舉成員賦予自己型別的一個問題是,這些型別在某種程度上與成員的實際值相關聯。在某些情況下,不可能計算出該值——例如,列舉成員可以透過函式呼叫來初始化。
tsenum E {Blah = Math.random()}
每當 TypeScript 遇到這些問題時,它都會悄悄地退回到舊的列舉策略。這意味著放棄了聯合和字面量型別的所有優勢。
TypeScript 5.0 透過為每個計算成員建立唯一型別,成功地使所有列舉都成為聯合列舉。這意味著所有列舉現在都可以進行型別縮減,並且它們的成員也可以作為型別被引用。
有關此更改的更多詳細資訊,你可以在 GitHub 上閱讀具體內容。
--moduleResolution bundler
TypeScript 4.7 為其 --module 和 --moduleResolution 設定引入了 node16 和 nodenext 選項。這些選項的目的是更好地模擬 Node.js 中 ECMAScript 模組的精確查詢規則;然而,這種模式有許多其他工具並不真正強制執行的限制。
例如,在 Node.js 的 ECMAScript 模組中,任何相對匯入都需要包含副檔名。
js// entry.mjsimport * as utils from "./utils"; // ❌ wrong - we need to include the file extension.import * as utils from "./utils.mjs"; // ✅ works
Node.js 和瀏覽器這樣做有一定的原因——它使檔案查詢更快,並且更適合簡單的檔案伺服器。但對於許多使用 bundler(打包器)等工具的開發者來說,node16/nodenext 設定很麻煩,因為 bundler 沒有大多數這些限制。在某些方面,node 解析模式對於使用 bundler 的人來說更好。
但在某些方面,原始的 node 解析模式已經過時了。大多數現代 bundler 使用 Node.js 中 ECMAScript 模組和 CommonJS 查詢規則的融合。例如,無副檔名匯入就像在 CommonJS 中一樣工作得很好,但在瀏覽包的 export 條件時,它們會像在 ECMAScript 檔案中一樣更喜歡 import 條件。
為了模擬 bundler 的工作方式,TypeScript 現在引入了一種新策略:--moduleResolution bundler。
jsonc{"compilerOptions": {"target": "esnext","moduleResolution": "bundler"}}
如果你使用的是 Vite、esbuild、swc、Webpack、Parcel 等實現混合查詢策略的現代 bundler,那麼新的 bundler 選項應該非常適合你。
另一方面,如果你正在編寫一個打算釋出到 npm 的庫,使用 bundler 選項可能會隱藏對不使用 bundler 的使用者可能產生的問題。因此在這些情況下,使用 node16 或 nodenext 解析選項可能是一個更好的途徑。
要閱讀關於 --moduleResolution bundler 的更多資訊,請檢視實現拉取請求。
解析自定義標誌
JavaScript 工具現在可能模擬“混合”解析規則,就像我們在上面描述的 bundler 模式中那樣。因為工具在支援方面可能略有不同,TypeScript 5.0 提供了啟用或停用一些可能與你的配置工作或不工作的功能的方法。
allowImportingTsExtensions
--allowImportingTsExtensions 允許 TypeScript 檔案透過 TypeScript 特定的副檔名(如 .ts、.mts 或 .tsx)相互匯入。
此標誌僅在啟用了 --noEmit 或 --emitDeclarationOnly 時才允許使用,因為這些匯入路徑在 JavaScript 輸出檔案中無法在執行時解析。這裡的期望是你的解析器(例如你的 bundler、執行時環境或其他工具)將使這些 .ts 檔案之間的匯入生效。
resolvePackageJsonExports
--resolvePackageJsonExports 強制 TypeScript 在讀取 node_modules 中的包時查閱 package.json 檔案的 exports 欄位。
此選項在 --moduleResolution 的 node16、nodenext 和 bundler 選項下預設開啟。
resolvePackageJsonImports
--resolvePackageJsonImports 強制 TypeScript 在執行以 # 開頭的查詢時,如果該檔案所在的祖先目錄包含 package.json,則查閱 package.json 檔案的 imports 欄位。
此選項在 --moduleResolution 的 node16、nodenext 和 bundler 選項下預設開啟。
allowArbitraryExtensions
在 TypeScript 5.0 中,當匯入路徑以已知的 JavaScript 或 TypeScript 副檔名以外的副檔名結尾時,編譯器將查詢該路徑的宣告檔案,形式為 {檔案基本名}.d.{副檔名}.ts。例如,如果你在 bundler 專案中使用 CSS 載入器,你可能想為這些樣式表編寫(或生成)宣告檔案
css/* app.css */.cookie-banner {display: none;}
ts// app.d.css.tsdeclare const css: {cookieBanner: string;};export default css;
ts// App.tsximport styles from "./app.css";styles.cookieBanner; // string
預設情況下,此匯入會引發錯誤,讓你知道 TypeScript 不理解此檔案型別,並且你的執行時可能不支援匯入它。但如果你已經配置了執行時或 bundler 來處理它,你可以使用新的 --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 或 imports 欄位解析時應該成功。這些條件會被新增到解析器預設使用的任何現有條件中。
例如,當此欄位在 tsconfig.json 中設定如下時
jsonc{"compilerOptions": {"target": "es2022","moduleResolution": "bundler","customConditions": ["my-condition"]}}
每當引用 package.json 中的 exports 或 imports 欄位時,TypeScript 都會考慮名為 my-condition 的條件。
因此,當從具有以下 package.json 的包中匯入時
jsonc{// ..."exports": {".": {"my-condition": "./foo.mjs","node": "./bar.mjs","import": "./baz.mjs","require": "./biz.mjs"}}}
TypeScript 將嘗試查詢與 foo.mjs 對應的檔案。
此欄位僅在 --moduleResolution 的 node16、nodenext 和 bundler 選項下有效
--verbatimModuleSyntax
預設情況下,TypeScript 執行所謂的匯入省略 (import elision)。基本上,如果你寫類似
tsimport { Car } from "./car";export function drive(car: Car) {// ...}
TypeScript 檢測到你僅將匯入用於型別,並完全刪除了該匯入。你的輸出 JavaScript 可能看起來像這樣
jsexport function drive(car) {// ...}
大多數情況下這很好,因為如果 Car 不是從 ./car 匯出的值,我們將得到執行時錯誤。
但它確實增加了一層複雜性,以處理某些極端情況。例如,請注意沒有像 import "./car"; 這樣的語句——匯入被完全刪除了。這實際上對有或沒有副作用的模組產生了影響。
TypeScript 的 JavaScript 編譯策略還有另外幾層複雜性——匯入省略並不總是僅僅由匯入的使用方式驅動——它通常也會參考值的宣告方式。因此,不清楚類似以下的程式碼
tsexport { Car } from "./car";
是應該被保留還是被刪除。如果 Car 是用 class 之類的東西宣告的,那麼它可以保留在生成的 JavaScript 檔案中。但如果 Car 僅宣告為 type 別名或 interface,那麼 JavaScript 檔案根本不應該匯出 Car。
雖然 TypeScript 能夠基於跨檔案的資訊做出這些編譯決策,但並非每個編譯器都能做到。
匯入和匯出上的 type 修飾符在一定程度上有所幫助。我們可以透過使用 type 修飾符明確匯入或匯出是否僅用於型別分析,以及是否可以完全從 JavaScript 檔案中刪除。
ts// This statement can be dropped entirely in JS outputimport type * as car from "./car";// The named import/export 'Car' can be dropped in JS outputimport { type Car } from "./car";export { type Car } from "./car";
type 修飾符本身並不是特別有用——預設情況下,模組省略仍會刪除匯入,並且沒有任何東西強制你區分 type 匯入/匯出和普通匯入/匯出。因此,TypeScript 提供了 --importsNotUsedAsValues 標誌來確保你使用 type 修飾符,--preserveValueImports 來防止某些模組省略行為,以及 --isolatedModules 來確保你的 TypeScript 程式碼可以在不同的編譯器中工作。不幸的是,理解這三個標誌的細節很困難,而且仍然存在一些具有意外行為的極端情況。
TypeScript 5.0 引入了一個名為 --verbatimModuleSyntax 的新選項來簡化這種情況。規則簡單得多——任何沒有 type 修飾符的匯入或匯出都會被保留。任何使用 type 修飾符的東西都會被完全刪除。
ts// Erased away entirely.import type { A } from "a";// Rewritten to 'import { b } from "bcd";'import { b, type c, type d } from "bcd";// Rewritten to 'import {} from "xyz";'import { type xyz } from "xyz";
有了這個新選項,所見即所得。
不過,這在模組互操作性方面確實有一些含義。在此標誌下,ECMAScript 的 import 和 export 在你的設定或副檔名隱含不同的模組系統時,不會被重寫為 require 呼叫。相反,你會得到一個錯誤。如果你需要編譯出使用 require 和 module.exports 的程式碼,你將必須使用 ES2015 之前的 TypeScript 模組語法
| 輸入 TypeScript | 輸出 JavaScript |
|---|---|
|
|
|
|
雖然這是一個限制,但它確實有助於使一些問題更加明顯。例如,在 --module node16 下忘記在 package.json 中設定 type 欄位是非常常見的。結果,開發人員會在沒有意識到的情況下開始編寫 CommonJS 模組而不是 ES 模組,從而導致令人驚訝的查詢規則和 JavaScript 輸出。這個新標誌確保你對使用的檔案型別是有意為之的,因為語法是有意不同的。
因為 --verbatimModuleSyntax 提供了比 --importsNotUsedAsValues 和 --preserveValueImports 更一致的解決方案,所以這兩個現有的標誌已被廢棄,轉而支援新標誌。
有關更多詳細資訊,請閱讀 [原始拉取請求](https://github.com/microsoft/TypeScript/pull/52203) 和 其提案問題。
支援 export type *
當 TypeScript 3.8 引入僅型別匯入時,新語法不允許用於 export * from "module" 或 export * as ns from "module" 重新匯出。TypeScript 5.0 增加了對這兩種形式的支援
ts// models/vehicles.tsexport class Spaceship {// ...}// models/index.tsexport type * as vehicles from "./vehicles";// main.tsimport { vehicles } from "./models";function takeASpaceship(s: vehicles.Spaceship) {// ✅ ok - `vehicles` only used in a type position}function makeASpaceship() {return new vehicles.Spaceship();// ^^^^^^^^// 'vehicles' cannot be used as a value because it was exported using 'export type'.}
你可以在這裡閱讀更多關於實現的細節。
JSDoc 中的 @satisfies 支援
TypeScript 4.9 引入了 satisfies 運算子。它確保表示式的型別相容,而不影響型別本身。例如,讓我們看以下程式碼
tsinterface 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 知道 myConfigSettings.extends 是以陣列形式宣告的——因為雖然 satisfies 驗證了我們物件的型別,但它並沒有生硬地將其更改為 CompilerOptions 並丟失資訊。所以如果我們想對 extends 進行對映,這是可以的。
tsdeclare function resolveConfig(configPath: string): CompilerOptions;let inheritedConfigs = myConfigSettings.extends.map(resolveConfig);
這對 TypeScript 使用者很有幫助,但許多人使用 TypeScript 透過 JSDoc 註釋來對 JavaScript 程式碼進行型別檢查。這就是為什麼 TypeScript 5.0 支援一個新的名為 @satisfies 的 JSDoc 標籤,它做的事情完全一樣。
/** @satisfies */ 可以捕獲型別不匹配
js// @ts-check/*** @typedef CompilerOptions* @prop {boolean} [strict]* @prop {string} [outDir]*//*** @satisfies {CompilerOptions}*/let myCompilerOptions = {outdir: "../lib",// ~~~~~~ oops! we meant outDir};
但它會保留我們表示式的原始型別,從而允許我們在程式碼中後續更精確地使用我們的值。
js// @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
tslet myConfigSettings = /** @satisfies {ConfigSettings} */ ({compilerOptions: {strict: true,outDir: "../lib",},extends: ["@tsconfig/strictest/tsconfig.json","../../../tsconfig.base.json"],});
為什麼?嗯,當你深入到其他程式碼(如函式呼叫)中時,這通常更有意義。
jscompileCode(/** @satisfies {CompilerOptions} */ ({// ...}));
此功能由 Oleksandr Tarasiuk 提供!
JSDoc 中的 @overload 支援
在 TypeScript 中,你可以為函式指定過載。過載為我們提供了一種說明函式可以用不同的引數呼叫,並可能返回不同結果的方法。它們可以限制呼叫者實際使用我們函式的方式,並細化他們將獲得的結果。
ts// Our overloads:function printValue(str: string): void;function printValue(num: number, maxFractionDigits?: number): void;// Our implementation: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 接收 string 或 number 作為其第一個引數。如果它接收一個 number,它可以接收第二個引數來決定我們可以列印多少個小數位。
TypeScript 5.0 現在允許 JSDoc 使用新的 @overload 標籤宣告過載。每個帶有 @overload 標籤的 JSDoc 註釋都被視為後續函式宣告的一個獨立過載。
js// @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 都能讓我們知道我們是否錯誤地呼叫了函式。
ts// all allowedprintValue("hello!");printValue(123.45);printValue(123.45, 2);printValue("hello!", 123); // error!
這個新標籤得以實現要感謝 Tomasz Lenarcik。
在 --build 下傳遞編譯特定標誌
TypeScript 現在允許在 --build 模式下傳遞以下標誌
--declaration--emitDeclarationOnly--declarationMap--sourceMap--inlineSourceMap
這使得在某些你可能擁有不同開發和生產構建的構建部分進行自定義變得容易多了。
例如,庫的開發構建可能不需要生成宣告檔案,但生產構建則需要。專案可以配置宣告輸出預設為關閉,並簡單地使用以下命令構建
shtsc --build -p ./my-project-dir
當你完成內部迴圈中的迭代後,“生產”構建只需傳遞 --declaration 標誌即可。
shtsc --build -p ./my-project-dir --declaration
編輯器中不區分大小寫的匯入排序
在 Visual Studio 和 VS Code 等編輯器中,TypeScript 為組織和排序匯入和匯出提供了支援。然而,通常對於列表何時“排序”存在不同的解釋。
例如,下面的匯入列表排序了嗎?
tsimport {Toggle,freeze,toBoolean,} from "./utils";
答案可能會令人驚訝地是“視情況而定”。如果我們不關心大小寫敏感性,那麼這個列表顯然沒有排序。字母 f 在 t 和 T 之前。
但在大多數程式語言中,排序預設為比較字串的位元組值。JavaScript 比較字串的方式意味著 "Toggle" 總是排在 "freeze" 之前,因為根據 ASCII 字元編碼,大寫字母排在小寫字母之前。所以從這個角度來看,匯入列表是排序的。
TypeScript 以前認為匯入列表已排序,因為它執行的是基本的區分大小寫的排序。對於喜歡不區分大小寫排序的開發者,或者使用需要預設不區分大小寫排序的 ESLint 等工具的開發者來說,這可能是一個令人沮喪的地方。
TypeScript 現在預設檢測大小寫敏感性。這意味著 TypeScript 和像 ESLint 這樣的工具通常不會因為如何最好地排序匯入而發生“衝突”。
我們的團隊也在試驗進一步的排序策略,你可以在這裡閱讀相關內容。這些選項最終可能會透過編輯器進行配置。目前,它們仍然不穩定且是實驗性的,你今天可以透過在 JSON 選項中使用 typescript.unstable 條目來選擇加入它們。以下是你可以嘗試的所有選項(設定為其預設值)
jsonc{"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 現在可以檢測正在檢查的值何時具有字面量型別。如果是這樣,它將提供一個自動補全,搭建出每個未覆蓋的 case。

你可以在 GitHub 上檢視實現的具體細節。
速度、記憶體和包大小最佳化
TypeScript 5.0 在我們的程式碼結構、資料結構和演算法實現方面包含了許多強大的更改。這意味著你的整體體驗應該更快——不僅是執行 TypeScript,甚至是安裝它。
以下是我們相對於 TypeScript 4.9 能夠實現的一些在速度和大小方面有趣的成果。
| 場景 | 相對於 TS 4.9 的時間或大小 |
|---|---|
| material-ui 構建時間 | 89% |
| TypeScript 編譯器啟動時間 | 89% |
| Playwright 構建時間 | 88% |
| TypeScript 編譯器自構建時間 | 87% |
| Outlook Web 構建時間 | 82% |
| VS Code 構建時間 | 80% |
| typescript npm 包大小 | 59% |

如何實現的?有一些值得注意的改進,我們希望在未來提供更多細節。但我們不會讓你等待那篇部落格文章。
首先,我們最近將 TypeScript 從名稱空間遷移到了模組,這使我們能夠利用可以執行範圍提升 (scope hoisting) 等最佳化的現代構建工具。使用這些工具、重新審視我們的打包策略並刪除一些已廢棄的程式碼,已經從 TypeScript 4.9 的 63.8 MB 包大小中削減了約 26.4 MB。它還透過直接函式呼叫為我們帶來了顯著的速度提升。
TypeScript 還增加了編譯器內部物件型別的統一性,並且還簡化了這些物件型別上儲存的資料。這減少了多型和超多型 (megamorphic) 的使用點,同時抵消了統一形狀所需的大部分記憶體消耗。
我們還在將資訊序列化為字串時執行了一些快取。型別顯示(可能作為錯誤報告、宣告輸出、程式碼完成等的一部分發生)最終可能相當昂貴。TypeScript 現在快取了一些常用機制,以便在這些操作中重複使用。
我們在解析器方面所做的另一個顯著更改是利用 var 來偶爾繞過在閉包中使用 let 和 const 的成本。這提高了一些解析效能。
總的來說,我們預計大多數程式碼庫應該能從 TypeScript 5.0 中看到速度提升,並且我們能夠持續復現 10% 到 20% 的效能提升。當然,這將取決於硬體和程式碼庫特徵,但我們鼓勵你今天就在你的程式碼庫中試用它!
有關更多資訊,請參閱我們的一些顯著最佳化
破壞性更改和廢棄
執行時要求
TypeScript 現在以 ECMAScript 2018 為目標。對於 Node 使用者,這意味著至少需要 Node.js 10 及更高版本。
lib.d.ts 變更
DOM 型別生成方式的更改可能會影響現有程式碼。值得注意的是,某些屬性已從 number 轉換為數字字面量型別,並且剪下、複製和貼上事件處理的屬性和方法已在介面之間移動。
API 破壞性更改
在 TypeScript 5.0 中,我們遷移到了模組,刪除了一些不必要的介面,並進行了一些正確性改進。有關已更改內容的更多詳細資訊,請參閱我們的 API 破壞性更改頁面。
關係運算符中禁止隱式強制轉換
TypeScript 中的某些操作已經會在你編寫可能導致隱式字串轉數字強制轉換的程式碼時警告你
tsfunction func(ns: number | string) {return ns * 4; // Error, possible implicit coercion}
在 5.0 中,這也將應用於關係運算符 >、<、<= 和 >=
tsfunction func(ns: number | string) {return ns > 4; // Now also an error}
如果需要,你可以使用 + 顯式將運算元強制轉換為 number 來允許這樣做
tsfunction func(ns: number | string) {return +ns > 4; // OK}
此正確性改進由 Mateusz Burzyński 提供。
列舉徹底改革
自第一個版本釋出以來,TypeScript 在 enum(列舉)方面一直存在一些長期存在的問題。在 5.0 中,我們清理了一些此類問題,並減少了理解你可以宣告的各種 enum 型別所需的概念數量。
作為其中的一部分,你可能會看到兩個主要的錯誤。第一個是,將域外字面量賦值給 enum 型別現在會像預期那樣報錯
tsenum SomeEvenDigit {Zero = 0,Two = 2,Four = 4}// Now correctly an errorlet m: SomeEvenDigit = 1;
另一個是,某些型別的間接混合字串/數字 enum 形式的宣告會錯誤地建立一個全數字的 enum
tsenum Letters {A = "a"}enum Numbers {one = 1,two = Letters.A}// Now correctly an errorconst t: number = Numbers.two;
你可以在相關更改中檢視更多詳細資訊。
在 --experimentalDecorators 下對建構函式中的引數裝飾器進行更準確的型別檢查
TypeScript 5.0 使 --experimentalDecorators 下裝飾器的型別檢查更加準確。當在建構函式引數上使用裝飾器時,這一點變得顯而易見。
tsexport declare const inject:(entity: any) =>(target: object, key: string | symbol, index?: number) => void;export class Foo {}export class C {constructor(@inject(Foo) private x: any) {}}
此呼叫將失敗,因為 key 需要 string | symbol,但建構函式引數接收的鍵為 undefined。正確的修復是更改 inject 中 key 的型別。如果你使用的庫無法升級,一個合理的解決方法是將 inject 包裝在一個型別更安全的裝飾器函式中,並對 key 使用型別斷言。
有關更多詳細資訊,請檢視此問題。
廢棄和預設值更改
在 TypeScript 5.0 中,我們廢棄了以下設定和設定值
--target: ES3--out--noImplicitUseStrict--keyofStringsOnly--suppressExcessPropertyErrors--suppressImplicitAnyIndexErrors--noStrictGenericChecks--charset--importsNotUsedAsValues--preserveValueImports- 專案引用中的
prepend
這些配置將繼續被允許,直到 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(換行)行尾,因此新的預設設定是 LF。舊的作業系統特定的推斷行為已不再可用。
--forceConsistentCasingInFileNames(確保專案中對同一檔名的所有引用在大小寫上一致)現在預設為 true。這有助於捕獲在不區分大小寫的檔案系統上編寫程式碼時產生的問題。
你可以留下反饋並檢視有關 5.0 廢棄跟蹤問題的更多資訊