枚舉類型分享 第五節

在 JS 語言裏面並不存在語言層面的枚舉類型,而 TS 將枚舉類型添加到了語言的類型系統裏面,這樣做的優勢:

  1. 開發者更容易清晰的窮盡某個 case 的各種可能;
  2. 更容易以文檔的形式列出程序邏輯,增加可讀性;

一、整型枚舉

//數字型枚舉更貼近其他語言中設計的枚舉類型
enum Direction {
  Up = 1,
  Down,
  Left,
  Right,
}

上述枚舉類型的定義中,我們給 Up 賦值了 1,所有剩下的成員會採用自增長的方式被賦值,比如 Down 就為 2,Left=3, Right=4。
如果不給枚舉變量賦值,則數字型枚舉採用 0 開頭的索引,並自增長賦值。

枚舉的使用也很簡單,如下

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

數字型枚舉能與計算類型或者常量類型的枚舉變量混用。不過沒有初始化器(initializer)的成員要麼放在枚舉聲明的第一位,要麼跟在一個被明確賦值的數字枚舉變量後面,下面這種情況會報錯:

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

二、字符串型枚舉

字符串類型枚舉跟上述數字型枚舉差不多,但是在運行時稍微有些不易察覺的區別,下面我們展開說一下。

在字符串型枚舉裏面各個變量都必須被字符串字面量初始化或者被其他字符串型枚舉成員初始化,如下:

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

字符串類型的枚舉沒有默認的自增長機制(這是肯定的),但是也有其優勢,就是代碼再運行時,當我們查看運行時代碼的時候,字符串型枚舉每個枚舉變量的值都是清晰可閱讀的,反觀數字型枚舉,基本得不到什麼有用的信息。

三、異構型枚舉

示例如下:

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

單純的從技術實現上來講,字符串型和整型枚舉可以混合使用,但是這並不是一種比較明智的coding 方式,除非你真的想利用 JS 的運行時特點,否則不建議應用這種方式到日常代碼開發中。

四、計算型和常量類型枚舉變量

每個枚舉變量都會被賦值,這個變量值要麼是常量,要麼是計算後的值。一般下面的情況我們認為枚舉成員的值是常量:

  • 枚舉成員是在一個位置,並且沒有被顯示初始化,實際上是默認賦值為 0
// E.X is constant
enum E{
  X,
}
  • 枚舉成員沒有被顯示初始化,並且其前面的枚舉成員是整型,這種情況下,當前的枚舉成員被賦的值是前面枚舉成員值加 1
// All enum members in E1 and E2 are constant
enum E1{
  X,
  Y,
  Z,
}
enum E2{
  A = 1,
  B,
  C,
}
  • 枚舉成員被一個常量枚舉表達式賦值。一個常量枚舉表達式是在編譯階段能被完全計算出值的 TS 表達式的一個子集。如果滿足如下條件就可以認定為常量枚舉表達式:
  1. 一個字面量枚舉表達式(其實就是一個字符串字面量或者數字字面量)
  2. 一個對之前聲明的常量枚舉成員的引用
  3. 括號括起來的常量枚舉表達式
  4. +,-,~一元操作符 表達式
  5. +,-,*,/,%,<<,>>,>>>,&,|,^合法的二元操作表達式
  6. 如果一個常量表達式最後運算結果是NaN或者Infinity,TS 將會報告編譯時錯誤

除此之外其他所有情況都可視為計算型枚舉

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

五、聯合枚舉和枚舉成員類型

常量枚舉成員有一個不需要計算的特殊的子集:字面量枚舉成員。一個字面量枚舉成員是一個不需要初始化值的常量枚舉成員或者值是被以下情況初始化的:

  • 任意的字符串字面量(e.g."foo","bar,"baz"
  • 任意的數字字面量(e.g.1,100)
  • 任意數字字面量的一元操作表達式(e.g.-1,-100)

當一個枚舉類型裏面所有的成員都被賦值了字面量值,就會出現一些特殊的編程模式。

首先:枚舉成員也能變成類型。舉個栗子,我們可以看到如下某個成員變量僅僅擁有一個枚舉變量類型:

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'.
  radius: 100,
};

其次,枚舉類本身能變成每個枚舉成員的聯合類型。利用枚舉成員組成的聯合類型,類型檢測系統能清楚的知道枚舉本身內部存在的每個成員,TS 從而可以發現 bug,當我們對比錯誤的變量的時候。示例如下:

enum E {
  Foo,
  Bar,
}
function f(x: E) {
  if (x !== E.Foo || x !== E.Bar) {
//上述 if 判斷總是會返回 true,因為 E.Foo 和 E.Bar並沒有交集
// 首先檢查 x 是否不等於 E.Foo,如果檢查通過,根據邏輯運算的短路原則最終表達式判
// 定為 true,如果檢查不通過,那麼 x 的類型只能為 E.Foo,那麼 x !== E.Bar通過,
// 仍然返回 true
  }
}

六、 運行時(runtime)的枚舉

枚舉是存在在運行時(runtime)中的真實對象,例如:

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);

七、編譯時(compile-time)的枚舉

儘管枚舉在 js 運行時裏面是真實存在的對象,但是用 keyof 關鍵字處理枚舉得到的結果可能與標準的對象有所區別。事實上,用 keyof typeof 來獲取枚舉類型的時候,發現所有的枚舉的鍵都是 string 類型。

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");

八、反向映射

整型枚舉也可以反向映射,從枚舉成員的值映射為枚舉成員的變量名,舉個栗子:

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

TS 將上述邏輯轉譯為如下 js 邏輯:

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

注意:字符串型枚舉沒有反向映射

九、常量枚舉類型

大多數情況下,枚舉類型是十分有效的解決方案。但是在個別情況下,為了避免轉譯代碼帶來的額外的開銷以及訪問枚舉類型帶來的間接性,於是就出現了 const 關鍵字聲明的枚舉類型,如下:

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

常量枚舉類型只能使用常量枚舉表達式,同時也不想普通的枚舉類型,常量枚舉類型不會存在於運行時,會在編譯時被替換。常量枚舉類型的成員會在編譯時直接替換為其枚舉成員的值。

const enum Direction {
  Up,
  Down,
  Left,
  Right,
}
let directions = [
  Direction.Up,
  Direction.Down,
  Direction.Left,
  Direction.Right,
];
// 上述枚舉類型轉譯後代碼如下
"use strict";
let directions = [
    0 /* Up */,
    1 /* Down */,
    2 /* Left */,
    3 /* Right */,
];