列舉

列舉是 TypeScript 中少數幾個不是 JavaScript 類型層次延伸的功能之一。

列舉允許開發人員定義一組命名常數。使用列舉可以更輕鬆地記錄意圖,或建立一組不同的案例。TypeScript 提供數字和字串為基礎的列舉。

數字列舉

我們將首先從數字列舉開始,如果您來自其他語言,這可能更熟悉。可以使用 enum 關鍵字定義列舉。

ts
enum Direction {
Up = 1,
Down,
Left,
Right,
}
Try

在上面,我們有一個數字列舉,其中 Up 使用 1 初始化。從那時起,所有後續成員都會自動遞增。換句話說,Direction.Up 的值為 1Down 的值為 2Left 的值為 3Right 的值為 4

如果我們願意,我們可以完全省略初始化項

ts
enum Direction {
Up,
Down,
Left,
Right,
}
Try

在這裡,Up 的值為 0Down 的值為 1,依此類推。這種自動遞增行為對於我們可能不在乎成員值本身,但關心每個值與同一個列舉中的其他值不同的情況很有用。

使用列舉很簡單:只需將任何成員作為列舉本身的屬性進行訪問,並使用列舉的名稱宣告類型

ts
enum UserResponse {
No = 0,
Yes = 1,
}
 
function respond(recipient: string, message: UserResponse): void {
// ...
}
 
respond("Princess Caroline", UserResponse.Yes);
Try

數字列舉可以混合在 計算和常量成員中(見下文)。簡而言之,沒有初始化項的列舉需要排在第一位,或者必須排在使用數字常量或其他常量列舉成員初始化的數字列舉之後。換句話說,以下內容不被允許

ts
enum E {
A = getSomeValue(),
B,
Enum member must have initializer.1061Enum member must have initializer.
}
Try

字串列舉

字串列舉是一個類似的概念,但有一些微妙的 執行時期差異,如下所述。在字串列舉中,每個成員都必須使用字串文字或另一個字串列舉成員進行常量初始化。

ts
enum Direction {
Up = "UP",
Down = "DOWN",
Left = "LEFT",
Right = "RIGHT",
}
Try

儘管字串列舉沒有自動遞增行為,但字串列舉的好處是它們可以很好地「序列化」。換句話說,如果您在除錯並必須讀取數字列舉的執行時期值,則該值通常是不透明的 - 它本身不會傳達任何有用的含義(儘管 反向對應 通常可以提供幫助)。字串列舉允許您在程式碼執行時提供有意義且可讀取的值,而與列舉成員本身的名稱無關。

異質列舉

技術上,列舉可以與字串和數字成員混合,但為何要這樣做並不清楚

ts
enum BooleanLikeHeterogeneousEnum {
No = 0,
Yes = "YES",
}
Try

除非您真的試圖以巧妙的方式利用 JavaScript 的執行時間行為,否則建議您不要這樣做。

計算和常數成員

每個列舉成員都有與之關聯的值,該值可以是常數計算。如果列舉成員是

  • 列舉中的第一個成員,並且沒有初始化程式,則將其指定為值 0

    ts
    // E.X is constant:
    enum E {
    X,
    }
    Try
  • 沒有初始化程式,並且前一個列舉成員是數字常數。在這種情況下,當前列舉成員的值將是前一個列舉成員的值加一。

    ts
    // All enum members in 'E1' and 'E2' are constant.
     
    enum E1 {
    X,
    Y,
    Z,
    }
     
    enum E2 {
    A = 1,
    B,
    C,
    }
    Try
  • 列舉成員使用常數列舉表達式初始化。常數列舉表達式是 TypeScript 表達式的一個子集,可以在編譯時完全求值。如果表達式是

    1. 文字列舉表達式(基本上是字串文字或數字文字)
    2. 對先前定義的常數列舉成員的引用(可以來自不同的列舉)
    3. 括號中的常數列舉表達式
    4. 對常數列舉表達式應用 +-~ 單元運算子之一
    5. +-*/%<<>>>>>&|^ 二元運算子,常數列舉表達式作為運算元

    常數列舉表達式求值為 NaNInfinity 是編譯時錯誤。

在所有其他情況下,列舉成員被視為計算。

ts
enum FileAccess {
// constant members
None,
Read = 1 << 1,
Write = 1 << 2,
ReadWrite = Read | Write,
// computed member
G = "123".length,
}
Try

聯合列舉和列舉成員類型

有一組特殊的常數列舉成員未計算:文字列舉成員。文字列舉成員是沒有初始化值或初始化為以下值的常數列舉成員

  • 任何字串文字(例如:"foo""bar""baz"
  • 任何數字文字(例如:1100
  • 對任何數字文字套用一元減號(例如:-1-100

當列舉中的所有成員都有文字列舉值時,會有一些特殊語意發揮作用。

第一個是列舉成員也會同時變成型別!例如,我們可以說某些成員只能具有列舉成員的值

ts
enum ShapeKind {
Circle,
Square,
}
 
interface Circle {
kind: ShapeKind.Circle;
radius: number;
}
 
interface Square {
kind: ShapeKind.Square;
sideLength: number;
}
 
let c: Circle = {
kind: ShapeKind.Square,
Type 'ShapeKind.Square' is not assignable to type 'ShapeKind.Circle'.2322Type 'ShapeKind.Square' is not assignable to type 'ShapeKind.Circle'.
radius: 100,
};
Try

另一個變更則是列舉型別本身有效地變成每個列舉成員的聯集。對於聯集列舉,型別系統能夠利用它知道列舉本身中存在哪些精確值集合的事實。因此,TypeScript 可以偵測出我們可能不正確地比較值的錯誤。例如

ts
enum E {
Foo,
Bar,
}
 
function f(x: E) {
if (x !== E.Foo || x !== E.Bar) {
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.
//
}
}
Try

在該範例中,我們首先檢查 x 是否不是 E.Foo。如果該檢查成功,則我們的 || 將會短路,而且「if」的主體將會執行。然而,如果檢查未成功,則 x只能E.Foo,因此查看它是否不等於 E.Bar 沒有意義。

執行時期的列舉

列舉是執行時期存在的真實物件。例如,下列列舉

ts
enum E {
X,
Y,
Z,
}
Try

實際上可以傳遞給函式

ts
enum E {
X,
Y,
Z,
}
 
function f(obj: { X: number }) {
return obj.X;
}
 
// Works, since 'E' has a property named 'X' which is a number.
f(E);
Try

編譯時期的列舉

儘管列舉是執行階段存在的實體物件,但 keyof 關鍵字運作的方式與您對一般物件預期的不同。請改用 keyof typeof 來取得代表所有列舉鍵值(以字串形式)的類型。

ts
enum LogLevel {
ERROR,
WARN,
INFO,
DEBUG,
}
 
/**
* This is equivalent to:
* type LogLevelStrings = 'ERROR' | 'WARN' | 'INFO' | 'DEBUG';
*/
type LogLevelStrings = keyof typeof LogLevel;
 
function printImportant(key: LogLevelStrings, message: string) {
const num = 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");
Try

反向對應

除了建立一個物件,其屬性名稱為成員名稱之外,數字列舉成員也會取得從列舉值到列舉名稱的反向對應。例如,在此範例中

ts
enum Enum {
A,
}
 
let a = Enum.A;
let nameOfA = Enum[a]; // "A"
Try

TypeScript 編譯成下列 JavaScript

ts
"use strict";
var Enum;
(function (Enum) {
Enum[Enum["A"] = 0] = "A";
})(Enum || (Enum = {}));
let a = Enum.A;
let nameOfA = Enum[a]; // "A"
 
Try

在此產生的程式碼中,列舉會編譯成一個物件,用於儲存正向(name -> value)和反向(value -> name)對應。對其他列舉成員的參照總是發射為屬性存取,而不會內嵌。

請記住,字串列舉成員不會產生任何反向對應。

const 列舉

在多數情況下,列舉是完全有效的解決方案。但有時需求較為嚴格。為了避免在存取列舉值時付出額外產生程式碼和額外間接引用的代價,可以使用 const 列舉。Const 列舉使用 const 修飾詞定義在我們的列舉上

ts
const enum Enum {
A = 1,
B = A * 2,
}
Try

Const 列舉只能使用常數列舉運算式,而且與一般列舉不同,它們會在編譯期間完全移除。Const 列舉成員會在使用位置內嵌。這是可行的,因為 const 列舉無法有計算成員。

ts
const enum Direction {
Up,
Down,
Left,
Right,
}
 
let directions = [
Direction.Up,
Direction.Down,
Direction.Left,
Direction.Right,
];
Try

在產生的程式碼中會變成

ts
"use strict";
let directions = [
0 /* Direction.Up */,
1 /* Direction.Down */,
2 /* Direction.Left */,
3 /* Direction.Right */,
];
 
Try

Const 列舉陷阱

內嵌列舉值起初很簡單,但會帶來微妙的影響。這些陷阱僅與環境 const 列舉有關(基本上是 .d.ts 檔案中的 const 列舉),以及在專案之間共用它們,但如果您正在發布或使用 .d.ts 檔案,這些陷阱可能會適用於您,因為 tsc --declaration 會將 .ts 檔案轉換成 .d.ts 檔案。

  1. 由於 isolatedModules 文件 中列出的原因,該模式與環境常數列舉基本上不兼容。這表示如果您發布環境常數列舉,下游使用者將無法同時使用 isolatedModules 和那些列舉值。
  2. 您可以在編譯時輕鬆內嵌依賴項版本 A 的值,並在執行時匯入版本 B。如果您不夠小心,版本 A 和 B 的列舉可能會有不同的值,導致 令人驚訝的錯誤,例如採取 if 陳述式的錯誤分支。這些錯誤特別惡劣,因為在專案建置時通常會執行自動化測試,且版本與依賴項相同,這會完全遺漏這些錯誤。
  3. importsNotUsedAsValues: "preserve" 對於用作值的常數列舉不會省略匯入,但環境常數列舉無法保證執行時期的 .js 檔案存在。無法解析的匯入會在執行時期導致錯誤。明確省略匯入的常見方式,僅類型匯入目前不允許常數列舉值

以下兩種方法可避免這些陷阱

  1. 完全不使用常數列舉。您可以輕鬆地 禁止常數列舉,在 linter 的協助下。很明顯地,這會避免任何與常數列舉相關的問題,但會阻止您的專案內嵌自己的列舉。與內嵌其他專案的列舉不同,內嵌專案自己的列舉並不會造成問題,而且有效能上的影響。

  2. 不要發布環境常數列舉,透過 preserveConstEnums 的協助,取消其常數化。這是 TypeScript 專案本身 內部採取的方法。preserveConstEnums 會針對常數列舉發出與一般列舉相同的 JavaScript。然後,您可以在 建置步驟中 安全地從 .d.ts 檔案中移除 const 修飾詞。

    這樣下游使用者就不會從您的專案中內嵌列舉,避免上述缺點,但專案仍可以內嵌自己的列舉,這與完全禁止常數列舉不同。

環境列舉

環境列舉用於描述現有列舉類型的形狀。

ts
declare enum Enum {
A = 1,
B,
C = 2,
}
Try

環境列舉和非環境列舉之間的一個重要差異是,在一般列舉中,沒有初始化項目的成員如果其前面的列舉成員被視為常數,則將被視為常數。相反,沒有初始化項目的環境(且非常數)列舉成員始終被視為已計算。

物件與列舉

在現代 TypeScript 中,當具有 as const 的物件就足夠時,您可能不需要列舉

ts
const enum EDirection {
Up,
Down,
Left,
Right,
}
 
const ODirection = {
Up: 0,
Down: 1,
Left: 2,
Right: 3,
} as const;
 
EDirection.Up;
(enum member) EDirection.Up = 0
 
ODirection.Up;
(property) Up: 0
 
// Using the enum as a parameter
function walk(dir: EDirection) {}
 
// It requires an extra line to pull out the values
type Direction = typeof ODirection[keyof typeof ODirection];
function run(dir: Direction) {}
 
walk(EDirection.Left);
run(ODirection.Right);
Try

此格式優於 TypeScript 的 enum 的最大論點是,它讓您的程式碼庫與 JavaScript 的狀態保持一致,而且 當/如果 列舉新增到 JavaScript 中,您就可以轉移到額外的語法。

TypeScript 文件是一個開放原始碼專案。透過 傳送 Pull Request ❤ 來協助我們改善這些頁面

此頁面的貢獻者
OTOrta Therox (17)
FDG-SFrank de Groot - Schouten (1)
Ggreen961 (1)
TATex Andersen (1)
ETEric Telkkälä (1)
10+

最後更新時間:2024 年 3 月 21 日