列舉是 TypeScript 中少數幾個並非 JavaScript 型別級擴充套件的特性之一。
列舉允許開發者定義一組命名常量。使用列舉可以更輕鬆地記錄意圖,或建立一組不同的情況。TypeScript 提供了數字列舉和字串列舉。
數字列舉
我們首先從數字列舉開始,如果你來自其他語言,這可能比較熟悉。可以使用 enum 關鍵字定義列舉。
tsTryenumDirection {Up = 1,Down ,Left ,Right ,}
上面我們定義了一個數字列舉,其中 Up 被初始化為 1。此後的所有成員都會自動遞增。換句話說,Direction.Up 的值為 1,Down 為 2,Left 為 3,Right 為 4。
如果需要,我們也可以完全省略初始化器
tsTryenumDirection {Up ,Down ,Left ,Right ,}
在這裡,Up 的值將為 0,Down 為 1,依此類推。這種自動遞增的行為對於我們不關心成員具體值,但關心同一列舉中每個值與其他值不同時非常有用。
使用列舉很簡單:只需將列舉成員作為列舉本身的屬性進行訪問,並使用列舉名稱宣告型別即可。
tsTryenumUserResponse {No = 0,Yes = 1,}functionrespond (recipient : string,message :UserResponse ): void {// ...}respond ("Princess Caroline",UserResponse .Yes );
數字列舉可以與計算成員和常量成員(見下文)混合使用。簡單來說,沒有初始化器的列舉必須位於最前面,或者必須位於使用數值常量或其他常量列舉成員初始化的數字列舉之後。換句話說,以下寫法是不允許的
tsTryenumE {A =getSomeValue (),Enum member must have initializer.1061Enum member must have initializer., B }
字串列舉
字串列舉的概念相似,但在下文記錄的執行時差異上有一些細微差別。在字串列舉中,每個成員必須使用字串字面量或另一個字串列舉成員進行常量初始化。
tsTryenumDirection {Up = "UP",Down = "DOWN",Left = "LEFT",Right = "RIGHT",}
雖然字串列舉沒有自動遞增行為,但它的好處是“序列化”效果很好。換句話說,如果你在除錯時必須讀取數字列舉的執行時值,該值通常是不透明的——它本身無法傳達任何有用的意義(儘管反向對映通常有所幫助)。字串列舉允許你在程式碼執行時提供一個有意義且可讀的值,這與列舉成員名稱本身無關。
異構列舉
從技術上講,列舉可以混合字串和數字成員,但並不清楚為什麼要這樣做。
tsTryenumBooleanLikeHeterogeneousEnum {No = 0,Yes = "YES",}
除非你真的想以巧妙的方式利用 JavaScript 的執行時行為,否則建議不要這樣做。
計算成員和常量成員
每個列舉成員都有一個關聯的值,它可以是“常量”或“計算”出來的。如果滿足以下條件,列舉成員被視為常量:
-
它是列舉中的第一個成員且沒有初始化器,這種情況下它被賦值為
0。tsTry// E.X is constant:enumE {X ,} -
它沒有初始化器,且前一個列舉成員是“數字”常量。在這種情況下,當前列舉成員的值將是前一個列舉成員的值加一。
tsTry// All enum members in 'E1' and 'E2' are constant.enumE1 {X ,Y ,Z ,}enumE2 {A = 1,B ,C ,} -
列舉成員使用常量列舉表示式初始化。常量列舉表示式是 TypeScript 表示式的一個子集,可以在編譯時完全求值。如果一個表示式滿足以下條件,則它是常量列舉表示式:
- 它是字面量列舉表示式(基本上是字串字面量或數字字面量)
- 它是對先前定義的常量列舉成員的引用(可以來自不同的列舉)
- 它是帶括號的常量列舉表示式
- 它是應用於常量列舉表示式的
+,-,~一元運算子之一 - 它是使用常量列舉表示式作為運算元的
+,-,*,/,%,<<,>>,>>>,&,|,^二元運算子之一
常量列舉表示式求值為
NaN或Infinity是編譯時錯誤。
在所有其他情況下,列舉成員被視為計算成員。
tsTryenumFileAccess {// constant membersNone ,Read = 1 << 1,Write = 1 << 2,ReadWrite =Read |Write ,// computed memberG = "123".length ,}
聯合列舉與列舉成員型別
有一類特殊的常量列舉成員無需計算:字面量列舉成員。字面量列舉成員是沒有初始值或被初始化為以下值的常量列舉成員:
- 任何字串字面量(例如
"foo","bar","baz") - 任何數字字面量(例如
1,100) - 應用於任何數字字面量的一元減號(例如
-1,-100)
當列舉中的所有成員都具有字面量列舉值時,會產生一些特殊的語義。
首先是列舉成員也變成了型別!例如,我們可以說某些成員“只能”具有該列舉成員的值。
tsTryenumShapeKind {Circle ,Square ,}interfaceCircle {kind :ShapeKind .Circle ;radius : number;}interfaceSquare {kind :ShapeKind .Square ;sideLength : number;}letc :Circle = {Type 'ShapeKind.Square' is not assignable to type 'ShapeKind.Circle'.2322Type 'ShapeKind.Square' is not assignable to type 'ShapeKind.Circle'.: kind ShapeKind .Square ,radius : 100,};
另一個變化是,列舉型別本身實際上成為了每個列舉成員的“聯合”。有了聯合列舉,型別系統能夠利用它已知列舉中存在的所有精確值的集合這一事實。因此,TypeScript 可以捕獲我們可能錯誤地比較值的錯誤。例如
tsTryenumE {Foo ,Bar ,}functionf (x :E ) {if (This comparison appears to be unintentional because the types 'E.Foo' and 'E.Bar' have no overlap.2367This comparison appears to be unintentional because the types 'E.Foo' and 'E.Bar' have no overlap.x !==E .Foo ||x !==E .Bar ) {//}}
在那個例子中,我們首先檢查了 x 是不是 E.Foo。如果檢查成功,我們的 || 就會短路,`if` 的主體就會執行。但是,如果檢查沒有成功,那麼 x 就“只能”是 E.Foo,所以檢查它是否不等於 E.Bar 就沒有意義了。
執行時的列舉
列舉是執行時存在的真實物件。例如,以下列舉
tsTryenumE {X ,Y ,Z ,}
實際上可以傳遞給函式
tsTryenumE {X ,Y ,Z ,}functionf (obj : {X : number }) {returnobj .X ;}// Works, since 'E' has a property named 'X' which is a number.f (E );
編譯時的列舉
儘管列舉是執行時存在的真實物件,但 keyof 關鍵字的工作方式可能與你對典型物件的預期不同。請改用 keyof typeof 來獲取一個表示所有列舉鍵作為字串的型別。
tsTryenumLogLevel {ERROR ,WARN ,INFO ,DEBUG ,}/*** This is equivalent to:* type LogLevelStrings = 'ERROR' | 'WARN' | 'INFO' | 'DEBUG';*/typeLogLevelStrings = keyof typeofLogLevel ;functionprintImportant (key :LogLevelStrings ,message : string) {constnum =LogLevel [key ];if (num <=LogLevel .WARN ) {console .log ("Log level key is:",key );console .log ("Log level value is:",num );console .log ("Log level message is:",message );}}printImportant ("ERROR", "This is a message");
反向對映
除了建立一個帶有成員名稱作為屬性的物件外,數字列舉成員還獲得了一個從列舉值到列舉名稱的“反向對映”。例如,在這個例子中
tsTryenumEnum {A ,}leta =Enum .A ;letnameOfA =Enum [a ]; // "A"
TypeScript 將其編譯為以下 JavaScript
tsTry"use strict";var Enum;(function (Enum) {Enum[Enum["A"] = 0] = "A";})(Enum || (Enum = {}));let a = Enum.A;let nameOfA = Enum[a]; // "A"
在生成的程式碼中,列舉被編譯為一個儲存了正向(name -> value)和反向(value -> name)對映的物件。對其他列舉成員的引用總是被輸出為屬性訪問,而不會被內聯。
請記住,字串列舉成員“不會”生成反向對映。
const 列舉
在大多數情況下,列舉是一個完全有效的解決方案。然而,有時需求更嚴格。為了避免在訪問列舉值時支付額外生成的程式碼和額外間接定址的成本,可以使用 const 列舉。常量列舉使用 const 修飾符定義
tsTryconst enumEnum {A = 1,B =A * 2,}
常量列舉只能使用常量列舉表示式,且與普通列舉不同,它們在編譯過程中會被完全刪除。常量列舉成員在使用點被內聯。這是可能的,因為常量列舉不能有計算成員。
tsTryconst enumDirection {Up ,Down ,Left ,Right ,}letdirections = [Direction .Up ,Direction .Down ,Direction .Left ,Direction .Right ,];
在生成的程式碼中將變為
tsTry"use strict";let directions = [0 /* Direction.Up */,1 /* Direction.Down */,2 /* Direction.Left */,3 /* Direction.Right */,];
常量列舉的陷阱
內聯列舉值起初看起來很簡單,但會帶來細微的影響。這些陷阱僅適用於“環境”常量列舉(基本上是 .d.ts 檔案中的常量列舉)以及在專案之間共享它們的情況。但如果你正在釋出或消費 .d.ts 檔案,這些陷阱很可能適用於你,因為 tsc --declaration 會將 .ts 檔案轉換為 .d.ts 檔案。
- 由於
isolatedModules文件中列出的原因,該模式從根本上與環境常量列舉不相容。這意味著如果你釋出環境常量列舉,下游消費者將無法同時使用isolatedModules和這些列舉值。 - 你很容易在編譯時內聯依賴項 A 版本的值,而在執行時匯入 B 版本。如果不非常小心,版本 A 和 B 的列舉可能具有不同的值,從而導致令人驚訝的錯誤,例如在
if語句中進入錯誤的分支。這些錯誤尤其陰險,因為在專案構建時同時執行自動化測試是很常見的,且使用相同的依賴版本,這完全無法發現這些錯誤。 importsNotUsedAsValues: "preserve"不會刪除作為值使用的常量列舉的匯入,但環境常量列舉不能保證執行時.js檔案存在。不可解析的匯入會在執行時導致錯誤。目前通常用於明確刪除匯入的方式,即僅型別匯入,目前不允許使用常量列舉值。
這裡有兩種避免這些陷阱的方法
-
根本不要使用常量列舉。你可以很容易地在 Lint 工具的幫助下禁止常量列舉。顯然,這可以避免與常量列舉相關的任何問題,但會阻止你的專案內聯其自己的列舉。與內聯其他專案的列舉不同,內聯專案自己的列舉並不會帶來問題,反而有效能優勢。
-
不要釋出環境常量列舉,透過
preserveConstEnums對其進行“去常量化”。這是 TypeScript 專案本身內部採用的方法。preserveConstEnums為常量列舉生成的 JavaScript 與普通列舉相同。然後,你可以在構建步驟中安全地從.d.ts檔案中去除const修飾符。這樣下游消費者就不會內聯來自你專案的列舉,從而避免了上述陷阱,但專案仍然可以內聯其自己的列舉,這與完全禁止常量列舉不同。
環境列舉
環境列舉用於描述已存在的列舉型別的形狀。
tsTrydeclare enumEnum {A = 1,B ,C = 2,}
環境列舉和非環境列舉之間的一個重要區別是,在常規列舉中,如果前一個列舉成員被視為常量,則沒有初始化器的成員也被視為常量。相比之下,沒有初始化器的環境(非 const)列舉成員“總是”被視為計算成員。
物件與列舉
在現代 TypeScript 中,如果你可以使用帶有 as const 的物件,可能就不需要枚舉了
tsTryconst enumEDirection {Up ,Down ,Left ,Right ,}constODirection = {Up : 0,Down : 1,Left : 2,Right : 3,} asconst ;EDirection .Up ;ODirection .Up ;// Using the enum as a parameterfunctionwalk (dir :EDirection ) {}// It requires an extra line to pull out the valuestypeDirection = typeofODirection [keyof typeofODirection ];functionrun (dir :Direction ) {}walk (EDirection .Left );run (ODirection .Right );
與 TypeScript 的 enum 相比,支援這種格式的最大論據是,它使你的程式碼庫與 JavaScript 的現狀保持一致,並且如果/當列舉被新增到 JavaScript 中時,你可以切換到相應的語法。