Enum 用來宣告一群 const,也就是一些不會變動的值,比較常用到的地方像是:處理狀態(success, processing, failed)、使用狀態(active, inactive, deleted)、角色(admin, general_user)等等。
Table of Contents
1. Enum 使用時機
通常會用在表示一群 constant,以狀態為例,比較直觀的想法,會直接用字串來判斷邏輯:
function onFinishProcessing(status) {
switch (status) {
case 'success': {
console.log('Job process successfully')
break
}
case 'processing': {
console.log('Job is still processing')
break
}
case 'failed': {
console.log('Job failed')
break
}
}
}
這個方法可能會造成的問題是,當你在其他地方呼叫這個 function 時,如果有 typo 的話,可能就會有不預期的結果,ex:
onFinishProcessing('process')
// output nothing
考量到這個問題,通常會另外宣告變數來處理這個問題:
const SUCCESS = 'success'
const PROCESSING = 'processing'
const FAILED = 'failed'
function onFinishProcessing(status) {
switch (status) {
case SUCCESS: {
console.log('Job process successfully')
break
}
case PROCESSING: {
console.log('Job is still processing')
break
}
case FAILED: {
console.log('Job failed')
break
}
}
}
呼叫的時候就可以直接將這些變數作為 arguments 傳入:
onFinishProcessing(PROCESSING)
// Job is still processing
然而當狀態慢慢增加時,需要宣告的變數也會慢慢增加,假設現在有十個狀態,就需要宣告十個變數,而你其實也無法限制其他工程師要傳入的 argument,即便宣告了變數,其他工程師還是可以直接傳 value 進去 function 做宣告:onFinishProcessing('process')
。
當然你可能想到在 js 中可以用 object 搭配 Object.freeze()
來處理這個問題,後續會提到在 TypeScript 中用 Object 和 Enum 的差別,不過在 TypeScript 中我們會用 enum 來處理這個問題。
enum Status {
Success,
Processing,
Failed
}
function onFinishProcessing(status: Status) {
switch (status) {
case Status.Success: {
console.log('Job process successfully')
break
}
case Status.Processing: {
console.log('Job is still processing')
break
}
case Status.Failed: {
console.log('Job failed')
break
}
}
}
可以看到不但可以維持可讀性,還可以限制工程師在執行這個 function 的時候,只能傳入 Status enum 底下的 value,如果輸入其他 value 作為 argument 就會噴錯:
onFinishProcessing(Status.Success)
// Job process successfully
onFinishProcessing('process')
// Error: Argument of type '"process"' is not assignable to parameter of type 'Status'.ts(2345)
2. Enum 種類
2.1 Numeric Enum
enum 的種類預設是 Numeric,也就是說在存取 enum 的 member 時,會按照順序從 0 開始,逐一遞增:
enum Status {
Success,
Processing,
Failed
}
console.log(Status.Success)
// 0
console.log(Status.Processing)
// 1
console.log(Status.Failed)
// 2
如果有指定 numeric value 的話,則會從該 value 逐一遞增
enum Status {
Success = 10,
Processing,
Failed
}
console.log(Status.Success)
// 10
console.log(Status.Processing)
// 11
console.log(Status.Failed)
// 12
也可以從中間再賦值:
enum Status {
Success,
Processing = 5,
Failed
}
console.log(Status.Success)
// 0
console.log(Status.Processing)
// 5
console.log(Status.Failed)
// 6
或是三個 value 都自定義:
enum Status {
Success = 1,
Processing = 3,
Failed = 5
}
console.log(Status.Success)
// 1
console.log(Status.Processing)
// 3
console.log(Status.Failed)
// 5
只要 enum 裡每個 member 的 value 都是 numeric,就可以稱為 numeric enum
2.2 String Enum
和 numeric enum 一樣,只要 enum member 所有的 value 都是 string,就可以稱為 string enum:
enum Status {
Success = 'status',
Processing = 'processing',
Failed = 'failed'
}
console.log(Status.Success)
// status
console.log(Status.Processing)
// processing
console.log(Status.Failed)
// failed
2.3 Heterogeneous Enum
TypeScript 的 enum 允許 numeric 和 string 混用,不過通常沒什麼意義,所以不常見到。
enum Status {
Success,
Processing = 'processing',
Failed = 2
}
console.log(Status.Success)
// 0
console.log(Status.Processing)
// processing
console.log(Status.Failed)
// 2
3. Enum at Compile Time
3.1 Numeric Enum
Numeric enum member 的 name 和 value 是可以互相 mapping 到的(reverse mapping)
enum Status {
Success,
Processing,
Failed
}
const statusValue = Status.Success
const nameOfStatus = Status[statusValue]
// Success
可以觀察到 numeric enum 在 compile 成 js 時,會 compile 成 key 和 value 互相對應的 object:
var Status;
(function (Status) {
Status[Status["Success"] = 0] = "Success";
Status[Status["Processing"] = 1] = "Processing";
Status[Status["Failed"] = 2] = "Failed";
})(Status || (Status = {}));
在 js 中,新增 object 的 key-value 時,會回傳 value,所以假設宣告了:Status["Success"] = 0
,是會回傳 0 的,因此如果我們將 Status 這個 enum 給直接 console 出來:
console.log(Status)
/*
{
'0': 'Success',
'1': 'Processing',
'2': 'Failed',
Success: 0,
Processing: 1,
Failed: 2
}
*/
可以發現這是個 key-value 互相成對的 object,可以用 key 找到 value,也可以用該 value 照到對應的 key。
3.2 String Enum
String enum 並不像 numeric enum 會有 reverse mapping:
enum SStatus {
Success = 'success',
Processing = 'processing',
Failed = 'failed'
}
const statusValue = SStatus.Success
const nameOfStatus = SStatus[statusValue]
// error TS2551: Property 'success' does not exist on type 'typeof SStatus'.
可以觀察到 compile 成 js 後,並沒有辦法用 value 找到對應的 key:
var SStatus;
(function (SStatus) {
SStatus["Success"] = "success";
SStatus["Processing"] = "processing";
SStatus["Failed"] = "failed";
})(SStatus || (SStatus = {}));
直接在 TypeScript console 出來:
console(SStatus)
// { Success: 'success', Processing: 'processing', Failed: 'failed' }
3.3 Const Enum
如果用 const 來宣告 enum,可以觀察到 compile 成 js 後,不會產生任何的 object,會直接將用到 enum 的地方,替換成 value:
const enum CStatus {
Success,
Processing,
Failed
}
console.log(CStatus.Success)
console.log(CStatus.Processing)
console.log(CStatus.Failed)
console.log(CStatus)
// 並不像 numeric enum, string enum,可以直接 console 出來,因為並不會產生 object
// error TS2475: 'const' enums can only be used in property or index access expressions or the right hand side of an import declaration or export assignment or type query.
compile 成 js 後,可以發現用到 const enum 的地方會直接被 value 換掉,留下 comment 註解是哪個 const enum member 對應到的 value:
console.log(0 /* Success */);
console.log(1 /* Processing */);
console.log(2 /* Failed */);
使用 const enum 的好處:
- 效能比較好,不用額外用 IIFE 宣告物件
- compile 後的 code 比較少
壞處:
- 因為不會產生 object,所以在使用的限制上比較多(無法 iterate, 反向查詢 key 等等)
4. Iterate Enum
因為 enum 其實是 compile 成 object,因此大部分的情況都可以用 iterate object 的方式來處理。
4.1 Numeric Enum
如上面提到的,因為 numeric enum 會有 reverse mapping,因此 iterate key 的話,也會同時把對應到的 value 給 iterate 出來:
enum Status {
Success,
Processing,
Failed
}
/*
{
'0': 'Success',
'1': 'Processing',
'2': 'Failed',
Success: 0,
Processing: 1,
Failed: 2
}
*/
for (const key in Status) {
console.log(key)
}
/*
0
1
2
Success
Processing
Failed
*/
4.1.1 only iterate enum member name
因為 numeric enum 每個 member 的 value type 都會是 number,所以可以用 isNaN 來判斷:
for (const key in Status) {
if (isNaN(Number(key))) {
console.log(key)
}
}
/*
Success
Processing
Failed
*/
4.1.2 only iterate enum member value
for (const key in Status) {
if (!isNaN(Number(key))) {
console.log(key)
}
}
/*
0
1
2
*/
4.2 String Enum
string enum 比較單純,因為不支援 reverse mapping,所以視為一般的 object 來 iterate 即可
enum SStatus {
Success = 'success',
Processing = 'processing',
Failed = 'failed'
}
4.2.1 only iterate enum member name
for (const key in SStatus) {
console.log(key)
}
/*
Success
Processing
Failed
*/
4.2.2 only iterate enum member value
for (const key of Object.values(SStatus)) {
console.log(key)
}
/*
success
processing
failed
*/
5. Object vs Enum
最開頭有提到,既然 enum 最後都會被 compile 成 object,那為什麼不直接用 object 搭配 object.freeze()
就好?
其實最主要還是語法的簡潔程度,如果直接使用 object 的話,在某些情況要寫的 code 會變得複雜許多,ex:
enum EStatus {
Success,
Processing,
Failed
}
const OStatus = {
Success: 0,
Processing: 1,
Failed: 2,
}
console.log(Status.Success)
console.log(OStatus.Success)
function handleStatusCallback(status: EStatus) {}
// 會需要額外將 OStatus keys 的 type 取出來
type Status = typeof OStatus[keyof typeof OStatus]
function handleOStatusCallback(status: Status) {}
handleStatusCallback(EStatus.Success)
handleOStatusCallback(OStatus.Success)
6. 參考資料
TypeScript: Handbook – Enums
善用Enum 提高程式的可讀性- 基本用法feat. JavaScript
如果覺得我的文章有幫助的話,歡迎幫我的粉專按讚哦~謝謝你!