列舉是 TypeScript 中少數幾個不是 JavaScript 類型層次延伸的功能之一。
列舉允許開發人員定義一組命名常數。使用列舉可以更輕鬆地記錄意圖,或建立一組不同的案例。TypeScript 提供數字和字串為基礎的列舉。
數字列舉
我們將首先從數字列舉開始,如果您來自其他語言,這可能更熟悉。可以使用 enum
關鍵字定義列舉。
tsTry
enumDirection {Up = 1,Down ,Left ,Right ,}
在上面,我們有一個數字列舉,其中 Up
使用 1
初始化。從那時起,所有後續成員都會自動遞增。換句話說,Direction.Up
的值為 1
,Down
的值為 2
,Left
的值為 3
,Right
的值為 4
。
如果我們願意,我們可以完全省略初始化項
tsTry
enumDirection {Up ,Down ,Left ,Right ,}
在這裡,Up
的值為 0
,Down
的值為 1
,依此類推。這種自動遞增行為對於我們可能不在乎成員值本身,但關心每個值與同一個列舉中的其他值不同的情況很有用。
使用列舉很簡單:只需將任何成員作為列舉本身的屬性進行訪問,並使用列舉的名稱宣告類型
tsTry
enumUserResponse {No = 0,Yes = 1,}functionrespond (recipient : string,message :UserResponse ): void {// ...}respond ("Princess Caroline",UserResponse .Yes );
數字列舉可以混合在 計算和常量成員中(見下文)。簡而言之,沒有初始化項的列舉需要排在第一位,或者必須排在使用數字常量或其他常量列舉成員初始化的數字列舉之後。換句話說,以下內容不被允許
tsTry
enumE {A =getSomeValue (),Enum member must have initializer.1061Enum member must have initializer., B }
字串列舉
字串列舉是一個類似的概念,但有一些微妙的 執行時期差異,如下所述。在字串列舉中,每個成員都必須使用字串文字或另一個字串列舉成員進行常量初始化。
tsTry
enumDirection {Up = "UP",Down = "DOWN",Left = "LEFT",Right = "RIGHT",}
儘管字串列舉沒有自動遞增行為,但字串列舉的好處是它們可以很好地「序列化」。換句話說,如果您在除錯並必須讀取數字列舉的執行時期值,則該值通常是不透明的 - 它本身不會傳達任何有用的含義(儘管 反向對應 通常可以提供幫助)。字串列舉允許您在程式碼執行時提供有意義且可讀取的值,而與列舉成員本身的名稱無關。
異質列舉
技術上,列舉可以與字串和數字成員混合,但為何要這樣做並不清楚
tsTry
enumBooleanLikeHeterogeneousEnum {No = 0,Yes = "YES",}
除非您真的試圖以巧妙的方式利用 JavaScript 的執行時間行為,否則建議您不要這樣做。
計算和常數成員
每個列舉成員都有與之關聯的值,該值可以是常數或計算。如果列舉成員是
-
列舉中的第一個成員,並且沒有初始化程式,則將其指定為值
0
ts
Try// E.X is constant:enumE {X ,} -
沒有初始化程式,並且前一個列舉成員是數字常數。在這種情況下,當前列舉成員的值將是前一個列舉成員的值加一。
ts
Try// All enum members in 'E1' and 'E2' are constant.enumE1 {X ,Y ,Z ,}enumE2 {A = 1,B ,C ,} -
列舉成員使用常數列舉表達式初始化。常數列舉表達式是 TypeScript 表達式的一個子集,可以在編譯時完全求值。如果表達式是
- 文字列舉表達式(基本上是字串文字或數字文字)
- 對先前定義的常數列舉成員的引用(可以來自不同的列舉)
- 括號中的常數列舉表達式
- 對常數列舉表達式應用
+
、-
、~
單元運算子之一 +
、-
、*
、/
、%
、<<
、>>
、>>>
、&
、|
、^
二元運算子,常數列舉表達式作為運算元
常數列舉表達式求值為
NaN
或Infinity
是編譯時錯誤。
在所有其他情況下,列舉成員被視為計算。
tsTry
enumFileAccess {// constant membersNone ,Read = 1 << 1,Write = 1 << 2,ReadWrite =Read |Write ,// computed memberG = "123".length ,}
聯合列舉和列舉成員類型
有一組特殊的常數列舉成員未計算:文字列舉成員。文字列舉成員是沒有初始化值或初始化為以下值的常數列舉成員
- 任何字串文字(例如:
"foo"
、"bar"
、"baz"
) - 任何數字文字(例如:
1
、100
) - 對任何數字文字套用一元減號(例如:
-1
、-100
)
當列舉中的所有成員都有文字列舉值時,會有一些特殊語意發揮作用。
第一個是列舉成員也會同時變成型別!例如,我們可以說某些成員只能具有列舉成員的值
tsTry
enumShapeKind {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 可以偵測出我們可能不正確地比較值的錯誤。例如
tsTry
enumE {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
沒有意義。
執行時期的列舉
列舉是執行時期存在的真實物件。例如,下列列舉
tsTry
enumE {X ,Y ,Z ,}
實際上可以傳遞給函式
tsTry
enumE {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
來取得代表所有列舉鍵值(以字串形式)的類型。
tsTry
enumLogLevel {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");
反向對應
除了建立一個物件,其屬性名稱為成員名稱之外,數字列舉成員也會取得從列舉值到列舉名稱的反向對應。例如,在此範例中
tsTry
enumEnum {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 列舉使用 const
修飾詞定義在我們的列舉上
tsTry
const enumEnum {A = 1,B =A * 2,}
Const 列舉只能使用常數列舉運算式,而且與一般列舉不同,它們會在編譯期間完全移除。Const 列舉成員會在使用位置內嵌。這是可行的,因為 const 列舉無法有計算成員。
tsTry
const 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 */,];
Const 列舉陷阱
內嵌列舉值起初很簡單,但會帶來微妙的影響。這些陷阱僅與環境 const 列舉有關(基本上是 .d.ts
檔案中的 const 列舉),以及在專案之間共用它們,但如果您正在發布或使用 .d.ts
檔案,這些陷阱可能會適用於您,因為 tsc --declaration
會將 .ts
檔案轉換成 .d.ts
檔案。
- 由於
isolatedModules
文件 中列出的原因,該模式與環境常數列舉基本上不兼容。這表示如果您發布環境常數列舉,下游使用者將無法同時使用isolatedModules
和那些列舉值。 - 您可以在編譯時輕鬆內嵌依賴項版本 A 的值,並在執行時匯入版本 B。如果您不夠小心,版本 A 和 B 的列舉可能會有不同的值,導致 令人驚訝的錯誤,例如採取
if
陳述式的錯誤分支。這些錯誤特別惡劣,因為在專案建置時通常會執行自動化測試,且版本與依賴項相同,這會完全遺漏這些錯誤。 importsNotUsedAsValues: "preserve"
對於用作值的常數列舉不會省略匯入,但環境常數列舉無法保證執行時期的.js
檔案存在。無法解析的匯入會在執行時期導致錯誤。明確省略匯入的常見方式,僅類型匯入,目前不允許常數列舉值。
以下兩種方法可避免這些陷阱
-
完全不使用常數列舉。您可以輕鬆地 禁止常數列舉,在 linter 的協助下。很明顯地,這會避免任何與常數列舉相關的問題,但會阻止您的專案內嵌自己的列舉。與內嵌其他專案的列舉不同,內嵌專案自己的列舉並不會造成問題,而且有效能上的影響。
-
不要發布環境常數列舉,透過
preserveConstEnums
的協助,取消其常數化。這是 TypeScript 專案本身 內部採取的方法。preserveConstEnums
會針對常數列舉發出與一般列舉相同的 JavaScript。然後,您可以在 建置步驟中 安全地從.d.ts
檔案中移除const
修飾詞。這樣下游使用者就不會從您的專案中內嵌列舉,避免上述缺點,但專案仍可以內嵌自己的列舉,這與完全禁止常數列舉不同。
環境列舉
環境列舉用於描述現有列舉類型的形狀。
tsTry
declare enumEnum {A = 1,B ,C = 2,}
環境列舉和非環境列舉之間的一個重要差異是,在一般列舉中,沒有初始化項目的成員如果其前面的列舉成員被視為常數,則將被視為常數。相反,沒有初始化項目的環境(且非常數)列舉成員始終被視為已計算。
物件與列舉
在現代 TypeScript 中,當具有 as const
的物件就足夠時,您可能不需要列舉
tsTry
const 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 中,您就可以轉移到額外的語法。