Enum

TypeScript 筆記 – Enum

Enum 用來宣告一群 const,也就是一些不會變動的值,比較常用到的地方像是:處理狀態(success, processing, failed)、使用狀態(active, inactive, deleted)、角色(admin, general_user)等等。

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

如果覺得我的文章有幫助的話,歡迎幫我的粉專按讚哦~謝謝你!

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top