TypeScript —— 枚舉類型 enum 的紅與黑

TypeScript 設計的初衷是 JavaScript + Types,所有 TypeScript 的特性不改變運行時的行爲

反過來說,如果在 TS 代碼中去掉靜態類型,應該得到一份完整有效的 JS 代碼

這樣的好處在於,我們可以通過 ESbuild 而不是 tsc 完成我們的 TS 代碼到 JS 代碼的轉換

但實際上 TypeScript 中有一個特殊類型破壞了這種構想,它就是 Enum

 

 

一、什麼是 Enum

在 TypeScript 中可以通過 enum 來定義一組常量,並將這些常量放到同一個對象中管理:

enum Language {
  ZH_CN = 'zh_CN',
  ZH_HK = 'zh_HK',
  ZH_TW = 'zh_TW',
  EN_US = 'en_US',
  EN_GB = 'en_GB',
}

和 type、interface 類似,enum 可以直接作爲靜態類型使用

function getLocals(lang: Language) {
  return `hello ${lang}`;
}

但在調用這個函數的時候,傳入的參數不能是 enum 的值,而應該是 enum 的引用

 

從這裏就會發現 enum 的特性:可以當做對象使用

摘一段官方文檔的描述:枚舉類型在運行時會被編譯爲一個對象,包含正向映射(name -> value),如果是數值枚舉,還會生成反向映射(value -> name)

其實不只是運行時,普通的枚舉類型最終都會編譯爲對象

// 編譯前
enum Enum {
  A = 1,
  B = 2,
}

// 編譯後
var Enum;
(function (Enum) {
  // 因爲是數值枚舉,所以還生成了反向映射
  Enum[Enum["A"] = 1] = "A";
  Enum[Enum["B"] = 2] = "A";
})(Enum || (Enum = {}));

這時可以考慮使用 const enum 來優化編譯結果,它不會編譯未使用的枚舉項,而且不會生成對象,在編譯後只會保留枚舉值

// 編譯前
enum Enum {
  A = 1,
  B = 2,
}
const arr = [Enum.A]

// 編譯後
var arr = [1 /* A */];

 

 

二、Enum 的優缺點

由於 enum 可以當做對象使用,所以在管理常量上非常方便

比如上面的 Language,如果需要將 'zh_CN' 改爲 'zh_cn',最終只要調整一下 Language 中 ZH_CN 的值就行,因爲在使用的時候都是用的 Language.ZH_CN

 

除此之外,如果某個數據結構需要用到字符串和數字的雙向映射,這時候用 enum 會簡單很多,因爲數值枚舉會生成正向和反向映射

enum Options {
  apple = 1,
  pear = 2,
  lemon = 3,
  orange = 4,
}

console.log(Options[1]); // apple

 


 

而 enum 的缺點,就是在一開始提到的:違背了 TypeScript = JavaScript + Types 的構想

比如下面的這段 TS 代碼:

type DataItem = {
  label: string;
  value: number | string;
};

function formatLabels(arr: DataItem[]) {
  return Array.isArray(arr) ? arr.map((x) => x.label).join(', ') : '';
}

const data: DataItem[] = [
  { label: 'wise', value: 1 },
  { label: 'wrong', value: 2 },
];

formatLabels(data);

如果把 DataItem 刪掉,這段代碼就變成了完整的 JS 代碼

 

而下面這段使用 enum 的代碼

enum Language {
  ZH_CN = 'zh_CN',
  ZH_HK = 'zh_HK',
  ZH_TW = 'zh_TW',
  EN_US = 'en_US',
  EN_GB = 'en_GB',
}

function getLocals(lang: Language) {
  return `hello ${lang}`;
}

getLocals(Language.ZH_CN);

由於 enum 可以當做對象使用,所以如果刪掉 Language,這段代碼就無法運行

而且在作爲靜態類型使用的時候,enum 還會帶來額外的心智負擔

上面的 Language 如果換成聯合類型的寫法,可能更符合直覺:

type Language = 'zh_CN' | 'zh_HK' | 'zh_TW' | 'en_US' | 'en_GB';

最後,也是最大的缺點:由於使用了 enum,我們不得不使用 tsc 而非 ESbuild 來編譯項目,導致整個編譯過程的開銷巨大

 

 

三、可選的替代方案

如果很在意編譯過程的優化,可以考慮下面的替代方案

 

1. union type

type Language = 'zh_CN' | 'zh_HK' | 'zh_TW' | 'en_US' | 'en_GB';

function getLocals(lang: Language) {
  return `hello ${lang}`;
}

getLocals('zh_CN');

這個方案簡單粗暴,拋棄 enum 的特性,使用聯合類型來代替枚舉

其優點是通俗易懂,而且刪掉類型後就是一段正常的 JS 代碼

但缺點也很明顯,不容易維護。假如需要將 'zh_CN' 改爲 'zh_cn',那麼所有用到了 'zh_CN' 的地方都要調整

 

2. object as const

const LangConstant = {
  ZH_CN: 'zh_CN',
  ZH_HK: 'zh_HK',
  ZH_TW: 'zh_TW',
  EN_US: 'en_US',
  EN_GB: 'en_GB',
} as const;

type ValueOf<T> = T[keyof T];

type Language = ValueOf<typeof LangConstant>;
// "zh_CN" | "zh_HK" | "zh_TW" | "en_US" | "en_GB"

function getLocals(lang: Language) {
  return `hello ${lang}`;
}

getLocals(LangConstant.ZH_CN);

getLocals('zh_CN'); // Language 是一個聯合類型,所以這裏並不會報錯,但不推薦

直接創建一個 JS 對象來維護常量,這樣就解決了方案一不易維護的問題

然後通過 keyof 和 typeof 獲取到對象的值,並形成聯合類型

這段代碼刪掉靜態類型依然能夠正常運行。除了實現上稍微有點複雜以外,是一個很不錯的方案

 


不管是 union type 還是 object as const,其實都是對 enum 的吹毛求疵

如果項目不追求極致的編譯優化,大可以放心使用 enum;如果不需要反向映射,使用 const enum 或許是一個最優解

 

 

P.S. 關於 enum 的小技巧

1. 獲取枚舉的 key 類型

type LangKeys = keyof typeof Language;

 

2. 獲取枚舉的 value 類型

type LangValues = `${Language}`;

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章