一文讓你完全掌握 TS 枚舉

目前已出 重學 TS 專題(二十四篇),歡迎感興趣的小夥關注 全棧修仙之路一塊兒「重學TS」(掃描文末二維碼)。

1、基礎知識

在 JavaScript 中布爾類型的變量含有有限範圍的值,即 truefalse。而在 TypeScript 中使用枚舉,你也能夠自定義類似的類型。html

1.1 數字枚舉

這是一個枚舉的簡單示例:git

enum NoYes {
  No,
  Yes,
}

NoYes 被稱爲枚舉 NoYes 的成員。與對象字面量同樣,尾隨逗號是被容許的。對於 NoYes 枚舉咱們可以輕易的訪問它的成員,好比:github

function toChinese(value: NoYes) {
  switch (value) {
    case NoYes.No:
      return '否';
    case NoYes.Yes:
      return '是';
  }
}

assert.equal(toChinese(NoYes.No), '否');
assert.equal(toChinese(NoYes.Yes), '是');

1.1.1 枚舉成員值

每一個枚舉成員都有一個 name 和一個 value。數字枚舉成員值的默認類型是 number 類型。也就是說,每一個成員的值都是一個數字:正則表達式

enum NoYes {
  No,
  Yes,
}

assert.equal(NoYes.No, 0);
assert.equal(NoYes.Yes, 1);

除了讓 TypeScript 爲咱們指定枚舉成員的值以外,咱們還能夠手動賦值:typescript

enum NoYes {
  No = 0,
  Yes = 1,
}

這種經過等號的顯式賦值稱爲 initializer。若是枚舉中某個成員的值使用顯式方式賦值,但後續成員未顯示賦值, TypeScript 會基於當前成員的值加 1 做爲後續成員的值,好比如下 Enum 枚舉中的成員 C:segmentfault

enum Enum {
  A,
  B,
  C = 4,
  D,
  E = 8,
  F,
}

assert.deepEqual(
  [Enum.A, Enum.B, Enum.C, Enum.D, Enum.E, Enum.F],
  [0, 1, 4, 5, 8, 9]
);

1.2 枚舉成員名稱的轉換

常量的命名有幾種約定:安全

  • 傳統上,JavaScript 使用全大寫的名稱,這是它從 Java 和 C 繼承的約定: Number.MAX_VALUE
  • 衆所周知的 Symbol 用駝峯式表示,並以小寫字母開頭,由於它們與屬性名稱相關: Symbol.asyncIterator
  • TypeScript 手冊使用以大寫字母開頭的駝峯式名稱。這是標準的 TypeScript 風格,咱們將其用於 NoYes 枚舉。

1.3 引用枚舉成員名稱

與 JavaScript 對象相似,咱們可使用方括號來引用包含非法字符的枚舉成員:微信

enum HttpRequestField {
  'Accept',
  'Accept-Charset',
  'Accept-Datetime',
  'Accept-Encoding',
  'Accept-Language',
}

assert.equal(HttpRequestField['Accept-Charset'], 1);

1.4 基於字符串的枚舉

除了數字枚舉,咱們還可使用字符串做爲枚舉成員值:session

enum NoYes {
  No = 'No',
  Yes = 'Yes',
}

assert.equal(NoYes.No, 'No');
assert.equal(NoYes.Yes, 'Yes');

對於純字符串枚舉,咱們不能省略任何初始化程序。dom

1.5 異構枚舉

最後一種枚舉稱爲異構枚舉。異構枚舉的成員值是數字和字符串的混合:

enum Enum {
  A,
  B,
  C = 'C',
  D = 'D',
  E = 8,
  F,
}

assert.deepEqual(
  [Enum.A, Enum.B, Enum.C, Enum.D, Enum.E, Enum.F],
  [0, 1, 'C', 'D', 8, 9]
);

請注意,前面提到的規則也適用於此:若是先前的成員值爲數字,則咱們能省略初始化程序。異構枚舉因爲其應用較少而不多使用。

目前 TypeScript 只支持將數字和字符串做爲枚舉成員值。不容許使用其餘值,好比 symbols。

2、指定枚舉成員值

TypeScript 區分了三種指定枚舉成員值的方式:

  • 使用字面量進行初始化:

    • 隱式指定;
    • 經過數字字面量或字符串字面量。
  • 常量枚舉成員經過可在編譯時計算其結果的表達式初始化。
  • 計算的枚舉成員可經過任意表達式初始化。

2.1 字面量枚舉成員

若是枚舉只有字面量成員,咱們能夠將這些成員用做類型(相似於數字字面量能夠用做類型的方式):

enum NoYes {
  No = 'No',
  Yes = 'Yes',
}

function func(x: NoYes.No) {
  return x;
}

func(NoYes.No); // OK

//@ts-ignore: Argument of type '"No"' is not assignable to
//            parameter of type 'NoYes.No'.
func('No');

//@ts-ignore: Argument of type 'NoYes.Yes' is not assignable to
//            parameter of type 'NoYes.No'.
func(NoYes.Yes);

此外,字面量枚舉支持完整性檢查(咱們將在後面進行介紹)。

TypeScript 2.6 支持在 .ts 文件中經過在報錯一行上方使用 // @ts-ignore 來忽略錯誤。

// @ts-ignore 註釋會忽略下一行中產生的全部錯誤。建議實踐中在 @ts-ignore以後添加相關提示,解釋忽略了什麼錯誤。

請注意,這個註釋僅會隱藏報錯,而且咱們建議你少使用這一註釋。

2.2 const 枚舉成員

若是能夠在編譯時計算枚舉成員的值,則該枚舉成員是常量。所以,咱們能夠隱式指定其值(即,讓 TypeScript 爲咱們指定它的值)。或者咱們能夠顯式指定它的值,而且僅容許使用如下語法:

  • 數字字面量或字符串字面量
  • 對先前定義的常量枚舉成員的引用
  • 括號
  • 一元運算符 +-~
  • 二進制運算符 +-*/%<<>>>>>&|^

如下是一個成員都是常量的枚舉示例:

enum Perm {
  UserRead     = 1 << 8,
  UserWrite    = 1 << 7,
  UserExecute  = 1 << 6,
  GroupRead    = 1 << 5,
  GroupWrite   = 1 << 4,
  GroupExecute = 1 << 3,
  AllRead      = 1 << 2,
  AllWrite     = 1 << 1,
  AllExecute   = 1 << 0,
}

若是枚舉僅含有常量成員,則不能再將成員用做類型。可是咱們仍然能夠進行完整性檢查。

2.3 計算枚舉成員

能夠經過任意表達式設置枚舉成員的值。例如:

enum NoYesNum {
  No = 123,
  Yes = Math.random(), // OK
}

這是一個數字枚舉。字符串枚舉和異構枚舉會有更多的限制。例如,咱們不能調用某些方法來設定枚舉成員的值:

enum NoYesStr {
  No = 'No',
  //@ts-ignore: Computed values are not permitted in
  // an enum with string valued members.
  Yes = ['Y', 'e', 's'].join(''),
}

3、數字枚舉的缺點

3.1 缺點:日誌輸出

在輸出數字枚舉的成員時,咱們只會看到數字:

enum NoYes { No, Yes }

console.log(NoYes.No);
console.log(NoYes.Yes);

// Output:
// 0
// 1

3.2 缺點:鬆散型檢查

將枚舉用做類型時,容許的值不僅是枚舉成員的值 – 能夠接受任何數字:

enum NoYes { No, Yes }
function func(noYes: NoYes) {}

func(33); // no error!

爲何沒有更嚴格的靜態檢查?Daniel Rosenwasser解釋

該行爲是由按位運算引發的。有時 SomeFlag.Foo | SomeFlag.Bar 打算產生另外一種 SomeFlag。相反,您最終獲得了 number,而且你不想回退到 SomeFlag

我認爲,若是咱們再次運行 TypeScript 以後仍然有枚舉,那麼咱們將爲位標誌創建一個單獨的構造。

3.3 建議:使用字符串枚舉

個人建議是使用字符串枚舉:

enum NoYes { No='No', Yes='Yes' }

一方面,日誌輸出對人類更友好:

console.log(NoYes.No);
console.log(NoYes.Yes);

// Output:
// 'No'
// 'Yes'

另外一方面,咱們獲得更嚴格的類型檢查:

function func(noYes: NoYes) {}

//@ts-ignore: Argument of type '"abc"' is not assignable
//            to parameter of type 'NoYes'.
func('abc');

//@ts-ignore: Argument of type '"Yes"' is not assignable
//            to parameter of type 'NoYes'.
func('Yes');

4、枚舉的用例

4.1 用例:位模式

在 Node.js 文件系統模塊中,幾個函數具備參數模式。它的值用於經過 Unix 保留的編碼來指定文件權限:

  • 爲三類用戶指定了權限:

    • 用戶:文件的全部者
    • 組:與文件關聯的組的成員
    • 所有:全部人
  • 對於每一個類別,能夠授予如下權限:

    • r(讀取):容許類別中的用戶讀取文件
    • w(寫):容許類別中的用戶更改文件
    • x(執行):容許類別中的用戶執行文件

這意味着權限能夠用 9 位表示(3 個類別,每一個類別具備 3 個權限):

用戶 全部
權限 r,w,x r,w,x r,w,x
八、七、6 5 4 3 2 1 0

雖然在 Node.js 不是這樣作,可是咱們可使用一個枚舉來處理這些標誌:

enum Perm {
  UserRead     = 1 << 8, 
  UserWrite    = 1 << 7,
  UserExecute  = 1 << 6,
  GroupRead    = 1 << 5,
  GroupWrite   = 1 << 4,
  GroupExecute = 1 << 3,
  AllRead      = 1 << 2,
  AllWrite     = 1 << 1,
  AllExecute   = 1 << 0,
}

位模式經過按位或(OR)組合:

// User can change, read and execute; everyone else can only read and execute
assert.equal(
  Perm.UserRead | Perm.UserWrite | Perm.UserExecute |
  Perm.GroupRead | Perm.GroupExecute |
  Perm.AllRead | Perm.AllExecute,
  0o755);

// User can read and write; group members can read; everyone can’t access at all.
assert.equal(
  Perm.UserRead | Perm.UserWrite | Perm.GroupRead,
  0o640);
八進制,Octal,縮寫 OCT 或 O,一種以 8 爲基數的 計數法,採用 0,1,2,3,4,5,6,7 八個數字,逢八進 1。八進制 0o755 對應的十進制值是 493。

4.1.1 對位模式的替代

位模式背後的主要思想是存在一組標誌,而且能夠選擇這些標誌的任何子集。所以,使用 Set 選擇子集是執行同一任務的一種更具描述性的方式:

enum Perm {
  UserRead,
  UserWrite,
  UserExecute,
  GroupRead,
  GroupWrite,
  GroupExecute,
  AllRead,
  AllWrite,
  AllExecute,
}

function writeFileSync(
  thePath: string, permissions: Set<Perm>, content: string) {
  // ···
}

writeFileSync(
  '/tmp/hello.txt',
  new Set([Perm.UserRead, Perm.UserWrite, Perm.GroupRead]),
  'Hello!');

4.2 用例:多個常量

有時,咱們有一組屬於同類型的常量:

// Log level:
const off = Symbol('off');
const info = Symbol('info');
const warn = Symbol('warn');
const error = Symbol('error');

這是一個很好的枚舉用例:

enum LogLevel {
  off = 'off',
  info = 'info',
  warn = 'warn',
  error = 'error',
}

該枚舉的好處是:

  • 常量名稱被分組並嵌套在命名空間 LogLevel 內。
  • LogLevel 只要須要這些常量之一,就可使用類型,而且 TypeScript 會執行靜態檢查。

4.3 用例:相比布爾值來講更具自我描述性

當使用布爾值表示替代方案時,枚舉一般是一種更具自我描述性的選擇。

4.3.1 布爾型示例:有序列表與無序列表

例如,爲了表示列表是否有序,咱們可使用布爾值:

class List1 {
  isOrdered: boolean;
  // ···
}

可是,枚舉更具備自我描述性,並具備其餘好處,即若是須要,咱們能夠在之後添加更多選擇項。

enum ListKind { ordered, unordered }

class List2 {
  listKind: ListKind;
  // ···
}
4.3.2 布爾型示例:失敗與成功

一樣,咱們能夠經過布爾值或枚舉來表示操做是成功仍是失敗:

class Result1 {
  success: boolean;
  // ···
}

enum ResultStatus { failure, success }

class Result2 {
  status: ResultStatus;
  // ···
}

4.4 用例:更安全的字符串常量

考慮如下建立正則表達式的函數。

const GLOBAL = 'g';
const NOT_GLOBAL = '';

type Globalness = typeof GLOBAL | typeof NOT_GLOBAL;

function createRegExp(source: string,
  globalness: Globalness = NOT_GLOBAL) {
    return new RegExp(source, 'u' + globalness);
}

assert.deepEqual(
  createRegExp('abc', GLOBAL),
  /abc/ug);

若使用基於字符串的枚舉更爲方便:

enum Globalness {
  Global = 'g',
  notGlobal = '',
}

function createRegExp(source: string, globalness = Globalness.notGlobal) {
  return new RegExp(source, 'u' + globalness);
}

assert.deepEqual(
  createRegExp('abc', Globalness.Global),
  /abc/ug);

5、運行時枚舉

TypeScript 將枚舉編譯爲 JavaScript 對象。例如,定義如下枚舉:

enum NoYes {
  No,
  Yes,
}

TypeScript 將該枚舉編譯爲:

var NoYes;
(function (NoYes) {
  NoYes[NoYes["No"] = 0] = "No";
  NoYes[NoYes["Yes"] = 1] = "Yes";
})(NoYes || (NoYes = {}));

在此代碼中,進行了如下賦值操做:

NoYes["No"] = 0;
NoYes["Yes"] = 1;

NoYes[0] = "No";
NoYes[1] = "Yes";

有兩組賦值操做:

  • 前兩個賦值語句將枚舉成員名稱映射到值。
  • 後兩個賦值語句將值映射到名稱。這稱爲反向映射,咱們將在後面介紹。

5.1 反向映射

給定一個數字枚舉:

enum NoYes {
  No,
  Yes,
}

普通的映射是從成員名稱到成員值:

// 靜態查找
assert.equal(NoYes.Yes, 1);

// 動態查找
assert.equal(NoYes['Yes'], 1);

數字枚舉還支持從成員值到成員名稱的反向映射:

assert.equal(NoYes[1], 'Yes');

5.2 運行時基於字符串的枚舉

基於字符串的枚舉在運行時具備更簡單的表示形式。

考慮如下枚舉:

enum NoYes {
  No = 'NO!',
  Yes = 'YES!',
}

它會被編譯爲如下 JavaScript 代碼:

var NoYes;
(function (NoYes) {
    NoYes["No"] = "NO!";
    NoYes["Yes"] = "YES!";
})(NoYes || (NoYes = {}));

TypeScript 不支持基於字符串枚舉的反向映射。

6、const 枚舉

若是枚舉以 const 關鍵字爲前綴,則在運行時沒有任何表示形式,而是直接使用成員的值。

6.1 編譯非 const 枚舉

首先咱們來看一下非 const 枚舉:

enum NoYes {
  No,
  Yes,
}

function toChinese(value: NoYes) {
  switch (value) {
    case NoYes.No:
      return '否';
    case NoYes.Yes:
      return '是';
  }
}

TypeScript 會將以上代碼編譯爲:

var NoYes;
(function (NoYes) {
    NoYes[NoYes["No"] = 0] = "No";
    NoYes[NoYes["Yes"] = 1] = "Yes";
})(NoYes || (NoYes = {}));
function toChinese(value) {
    switch (value) {
        case NoYes.No:
            return '否';
        case NoYes.Yes:
            return '是';
    }
}

6.2 編譯 const 枚舉

這與前面的代碼基本一致,可是使用了 const 關鍵字:

const enum NoYes {
  No,
  Yes,
}

function toChinese(value: NoYes) {
  switch (value) {
    case NoYes.No:
      return '否';
    case NoYes.Yes:
      return '是';
  }
}

如今,以前生成的 NoYes 對象消失了,僅保留了其成員的值:

function toChinese(value) {
    switch (value) {
        case 0 /* No */:
            return '否';
        case 1 /* Yes */:
            return '是';
    }
}

7、編譯時枚舉

7.1 枚舉是對象

TypeScript 將(非 const)枚舉視爲對象:

enum NoYes {
  No = 'No',
  Yes = 'Yes',
}

function func(obj: { No: string }) {
  return obj.No;
}

assert.equal(
  func(NoYes),
  'No');

7.2 字面量枚舉全面性檢查

當咱們接受一個枚舉成員值時,咱們一般要確保:

  • 咱們沒有收到非法的值;
  • 咱們沒有遺漏任何枚舉成員的值。(若是之後再添加新的枚舉成員時,這一點尤其重要。)
7.2.1 抵禦非法值

在如下代碼中,咱們針對非法值採起了兩種措施:

enum NoYes {
  No = 'No',
  Yes = 'Yes',
}

function toChinese(value: NoYes) {
  switch (value) {
    case NoYes.No:
      return '否';
    case NoYes.Yes:
      return '是';
    default:
      throw new TypeError('Unsupported value: ' + JSON.stringify(value));
  }
}

assert.throws(
  //@ts-ignore: Argument of type '"Maybe"' is not assignable to
  //            parameter of type 'NoYes'.
  () => toChinese('Maybe'),
  /^TypeError: Unsupported value: "Maybe"$/);

這些措施是:

  • 在編譯時,該類型 NoYes 可防止將非法值傳遞給 value 參數;
  • 在運行時,若是含有其它值,則 default 分支會拋出異常。
7.2.2 經過全面性檢查抵禦遺漏場景

咱們能夠再採起一種措施。如下代碼執行全面性檢查:若是咱們忘記考慮全部枚舉成員,TypeScript 將警告咱們。

enum NoYes {
  No = 'No',
  Yes = 'Yes',
}

function throwUnsupportedValue(value: never): never {
  throw new TypeError('Unsupported value: ' + value);
}

function toChinese2(value: NoYes) {
  switch (value) {
    case NoYes.No:
      return '否';
    case NoYes.Yes:
      return '是';
    default:
      throwUnsupportedValue(value);
  }
}

全面性檢查如何工做?對於每種狀況,TypeScript 都會推斷 value 的類型:

function toGerman2b(value: NoYes) {
  switch (value) {
    case NoYes.No:
      const x: NoYes.No = value;
      return '否';
    case NoYes.Yes:
      const y: NoYes.Yes = value;
      return '是';
    default:
      const z: never = value;
      throwUnsupportedValue(value);
  }
}

在 default 分支中,TypeScript 會推斷 value 的類型爲 never 類型。可是,若是咱們添加一個成員 MaybeNoYes 枚舉中,以後 value 的推斷類型是 NoYes.Maybe,這時該變量的類型與 throwUnsupportedValue() 方法中參數的類型在靜態上不兼容。所以,咱們在編譯時會收到如下錯誤消息:

Argument of type 'NoYes.Maybe' is not assignable to parameter of type 'never'.

幸運的是,這種全面性檢查也適用於如下 if 語句:

function toGerman3(value: NoYes) {
  if (value === NoYes.No) {
    return '否';
  } else if (value === NoYes.Yes) {
    return '是';
  } else {
    throwUnsupportedValue(value);
  }
}
7.2.3 全面性檢查的另外一種方法

另外,若是咱們爲如下 toChinese() 函數指定返回類型,也能夠實現全面性檢查:

enum NoYes {
  No = 'No',
  Yes = 'Yes',
}

function toChinese(value: NoYes): string {
  switch (value) {
    case NoYes.No:
      const x: NoYes.No = value;
      return '否';
    case NoYes.Yes:
      const y: NoYes.Yes = value;
      return '是';
  }
}

若是咱們向 NoYes 中添加成員,則 TypeScript 會提醒 toChinese() 方法可能會返回 undefined

這種方法的缺點: 這種方法不適用於 if 語句。

7.3 keyof 和枚舉

咱們可使用 keyof 類型運算符建立類型,其元素是枚舉成員的 key。當咱們這樣作,咱們須要結合 keyoftypeof 一塊兒使用:

enum HttpRequestKeyEnum {
  'Accept',
  'Accept-Charset',
  'Accept-Datetime',
  'Accept-Encoding',
  'Accept-Language',
}

type HttpRequestKey = keyof typeof HttpRequestKeyEnum;
  // = 'Accept' | 'Accept-Charset' | 'Accept-Datetime' |
  //   'Accept-Encoding' | 'Accept-Language'

function getRequestHeaderValue(request: Request, key: HttpRequestKey) {
  // ···
}

爲何這樣?這比直接定義 HttpRequestKey 類型更方便。

7.3.1 使用 keyof 不使用 typeof

若是使用 keyof 不使用 typeof,則會獲得另外一個不太有用的類型:

type Keys = keyof HttpRequestKeyEnum;
  // = 'toString' | 'toFixed' | 'toExponential' |
  //   'toPrecision' | 'valueOf' | 'toLocaleString'

keyof HttpRequestKeyEnum 的結果與 keyof number 相同。

本文主要參考了「德國阮一峯」 —— Axel Rauschmayer 大神的 numeric-enums 這篇文章,感興趣的小夥伴可閱讀原文喲。
https://2ality.com/2020/01/ty...

8、參考資源

建立了一個 「重學TypeScript」 的微信羣,想加羣的小夥伴,加我微信 "semlinker",備註重學TS。

本人的全棧修仙之路訂閱號,會按期分享 Angular、TypeScript、Node.js/Java 、Spring 相關文章,歡迎感興趣的小夥伴訂閱哈!

full-stack-logo

相關文章
相關標籤/搜索