iT邦幫忙

第 11 屆 iThome 鐵人賽

DAY 12
4
自我挑戰組

Typescript 初心者手札系列 第 12

【Day 12】TypeScript 資料型別 - 元組(Tuple) & 列舉(Enum)

  • 分享至 

  • xImage
  •  

今天要來介紹一下 TS 有而 JS 沒有的資料型別 —— 元組(Tuple)和列舉(Enum)

元組(Tuple)

元組可以想成是一個嚴格的陣列,陣列的元素是固定數量的,且每個元素的型別都是已知的,但型別不用相同。

還記得怎麼進行陣列的型別註記嘛? 例如:想要創建一個可以含有數字和字串的陣列可以這樣寫

let arr:(string|number)[] = [18,'Kira']
or
let arr:(string|number)[] = ['Kira',18,'Yang',0988222111]

這時候,陣列裡面的元素不限數量,只要元素符合型別即可通過。那如果想要限制讓陣列的第一個索引值是數字,第二個是字串,第三個是布林值,要怎麼做呢?

這時候元組就可以出場啦!

//宣告一個元組陣列
let arr:[number,string,boolean];

//賦值,初始化陣列
arr = [10,'hello',true]; 

//初始化陣列錯誤
arr = [true, 10, 'hello']; //各索引值型別不符,報錯
arr = [10, 'hello']; //少了第三個元素,報錯
arr[0]='hi' //,型別限定為數字,報錯

你會發現每個索引值都有指定的型別,因此,只要一個索引值型別錯誤或是多或少了元素,TS都會逼逼,給他嚴格的檢查下去!

其註記方式可簡化為下面的公式:
變數 A 儲存了一元組陣列,元組內有 N 筆資料,每一元素值為 Dn,對應到型別 Tn

let A: [T1,T2,T3...Tn] = [D1,D2,D3...Dn]

咦,不知道大家看到這裡有沒有發現少了什麼? 沒錯!就是型別推論,只要遇到元組就一定會用型別註記,嚴格限制陣列中每個元素的型別。

這裡來比較一下陣列和元組的使用,假設有一個陣列存使用者基本資料(包含姓名和年齡),可以寫成下面:

//陣列型別
let person :(string | number)[]=['Una','Lin',18]
//對調元素位置 OK
person = [18,'Una','Lin']
//增加符合型別的新元素 OK
person = [18,'Una','Lin',0938333111]

//元組型別
let person :[string,string,number] =['Kira','Yang',18]

在陣列型別中,若將元素位置對調是可以的,甚至增加元素,只要是在規範的型別範疇內都可以 ; 但元組型別,每個元素的型別已固定,不可隨意調整元素位置(除非型別相同),也無法隨意增減元素。

來個重點整理吧:陣列與元組的差別

陣列只要內部的元素型別在註記的範圍內即可(例如 (string | number)[] 就只能存取字串跟數字),沒有限制元素的數量,順序也沒有限制;元組則是元素個數必須固定,各個元素格式也必須完全吻合。

列舉(Enum)

Enum 跟 Tuple 一樣是 TS 增加的新語法,也被稱做「列舉」或「枚舉」,Enum 可以想成就是將東西一個一個列出來,通常會用它來管理多個同系列的常數(不可修改的變數),將常數值一一列舉出來,且只能在列舉出來的範圍中選擇,例如:交通號誌顏色的列舉,元素就是是紅、綠、黃三個。列舉又分三種型別,分別為數字列舉(Number enum)字串列舉(String enum)異構列舉(Heterogeneous enum)

數字列舉(Number enum)

要如何定義數字列舉呢? 在 TS 中,會使用關鍵字 enum 來定義列舉,例如下方的程式碼:

enum Days {Sun, Mon, Tue, Wed, Thu, Fri, Sat}

放進 TS 中編譯,出來的結果如下:

var Days;
(function (Days) {
    Days[Days["Sun"] = 0] = "Sun";
    Days[Days["Mon"] = 1] = "Mon";
    Days[Days["Tue"] = 2] = "Tue";
    Days[Days["Wed"] = 3] = "Wed";
    Days[Days["Thu"] = 4] = "Thu";
    Days[Days["Fri"] = 5] = "Fri";
    Days[Days["Sat"] = 6] = "Sat";
})(Days || (Days = {}));

編譯後的程式碼,列舉被編譯成一個可查找的物件,但看起來好複雜!但仔細一看發現這不是 JS 的立即執行函式(IIFE)嗎?再看看內部重複的程式碼,只看第一段:

Days[Days["Sun"] = 0] = "Sun";

有點複雜,不過裡面的左右等號好像又可以拆解成:

Days[0] ="Sun"
Days["Sun"]=0

翻譯一下,就是說 Days 擁有數字 0 這個屬性對應 "Sun",且 Days 擁有 "Sun" 屬性對應 0,彼此的屬性相互對應,這就是數字列舉的重要特性-反向映射。

因此,我們可以這樣做

let dayOff: Days = Days.Sun
console.log(dayOff) //0 

//反推回來
let dayOfDayOff = Days[dayOff]
console.log(dayOfDayOff) //Sun

但這邊有一件事要注意,反推回來的時候 dayOfDayOff 是字串型別。

等等,我們原先定義列舉時,並沒有賦予元素值阿,那麼 0 到 6 又是哪來的呢?

在預設情況下,數字列舉會從 0 開始往上遞增賦值。當然,開發者也可以手動賦值,來測試以下幾種情境:

1. 第一個元素賦值為大於0的正數
enum Days {Sun=2, Mon, Tue, Wed, Thu, Fri, Sat}

2. 第二個元素賦值為負數
enum Days {Sun, Mon=-2, Tue, Wed, Thu, Fri, Sat}

3. 第二個元素賦值為小數
enum Days {Sun, Mon=1.6, Tue, Wed, Thu, Fri, Sat}

3. 賦值使元素值不連續
enum Days {Sun=3, Mon, Tue=1, Wed, Thu, Fri, Sat}

結果分別為:

  1. 可以看到未手動賦值的元素會接著上一個列舉元素遞增,從 2 開始遞增


2. 仍然遵循上面提到的規則,且 TS 列舉接受負值,但預設從數字 0 開始。不過,這裡發生了一個狀況,自動賦值的列舉元素 "Sun" 和 "Wed" 值都是 0 ,重複了,而 TS 並沒有報錯,這種狀況需要特別小心,盡量避免這種情況發生。


3. 可以知道 TS 的列舉元素也接受小數,後面元素加 1 遞增


4. 仍然遵循遞增規則,再次確認未手動賦值的元素會接著上一個賦值的列舉元素進行加 1 遞增。

另外,列舉中的元素可以是常數(constant)或是計算值(Computed) ,兩者的差異是在編譯時是否已知道值,計算值在編譯時是未知的,相反的,常數在編譯時是已知的。

在數字列舉中,+、 -、 ~ 等一元運算符以及二元運算符 +(加)、 -(減) 、*(乘)、 /(除)、 %(餘數)、 <<(左移運算符)、 >>(右移運算符)、 &(交集,類似and)、 |(聯集,類似or)都可以應用在常數表達式,使用其他的方法則被視為計算值。但如果常數表達式求值後為NaN或Infinity,會在編譯階段報錯。

舉例來說:

enum Animal{
  //被視為常數
  none      = 0,
  twoLegs   = 2 * 3,  //6
  fourLegs  = 1 << 1, //2
  CanSwim   = 1 << 2, //4
  CanFly    = 1 << 3  //8
  ReadWrite  = Read | Write,
  //被視為計算值
  G = "animl".length
}

這裡使用了左移的位運算符,將數字1的二進制向左移動位置得到數字 0010、0100 和 1000(換成十進制結果是2、4、8)。

字串列舉(String enum)

剛剛我們提到的都是數字列舉的部分,來看一下字串列舉。字串列舉的概念很簡單,在字串列舉中,每個元素都必須賦值字串資料來進行初始化:

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

編譯結果如下:

倘若沒有賦值,就會報錯 ; 如果字串相同,跟數字列舉一樣,TS 是不會檢查到的,因此,要特別小心。另外,要注意的是字串列舉沒有反向映射。

問題來了,那字串和數字元素可否在同一個列舉中呢?答案是可以的,這就是我們還沒提到的「異構列舉」(Heterogeneous enum)。

異構列舉(Heterogeneous enum)

舉例來說:

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

編譯出來的結果如下:

但雖然技術上來說是可以的,但官網並不建議使用。

常數列舉(const enum)

在 TS 中,如果希望所有元素都是常數,也可以使用常數列舉(const enum)來定義,使用方式是在 enum 關鍵字前使用 const 修飾符,範例程式碼如下:

const enum Direction {
  Up = 4,
  Down ,
  Left ,
  Right ,
}

let directions = Direction.Up;

編譯出來的結果是

var directions = 4 /* Up */; , 

加了 const 關鍵字後,列舉在編譯時不會產生查找物件,在常數列舉中無法使用計算值,且編譯時會將列舉的元素引用替換成其值。

倘若元素有計算值,就會報錯

const enum Direction {
  Up,
  Down ,
  Left ,
  Right ='123'.length ,
}

let result = Direction.Up; 
//Error: const enum member initializers can only contain literal values and other computed enum values.

常數列舉和上面的普通列舉最大的差異就是

  1. 常數列舉不會產生查找物件,可以提升效能
  2. 常數列舉的元素只能是常數,不能是運算值

倘若想保留常數列舉,也可以在 tsconfig.json 檔設定 preserveConstEnumstrue

外部列舉(Ambient enum)

外部列舉是用來描述已經存在列舉型別的輪廓,declare 在 TS 主要用來做聲明,
表示此物件已經存在,因此,我們不會使用 declare 來定義不存在的物件。通常使用 declare enum 會預期在其他地方會有實際執行的程式碼

外部列舉的使用範例如下:

declare enum Directions {
    Up = 1,  //常數
    Down, //計算值
    Left ,
    Right
}

let directions = Directions.Up;

編譯結果如下:

var directions = Directions.Up;

用 declare 方式宣告的列舉不會輸出查找物件。Directions.Up 會輸出 Directions.Up,但由於 declare 不會輸出查找物件,因此若其他地方沒有實際定義 enum,執行就會報錯。

倘若比較外部列舉和非外部列舉,在正常的列舉中,沒有賦值的成為常數元素; 而對於外部列舉來說,沒有賦值的元素被視為需要經過計算的。

若要同時使用 declare 和 const 也是可以的

declare const enum Directions {
    Up,
    Down,
    Left,
    Right
}

let directions = Directions.Up;

編譯出來會是:

var directions = 0 /* Up */;

這時候,一樣不會輸出查找物件,但在編譯時會將列舉的元素引用替換成其值,因此,Directions.Up 輸出會轉換為 0。

來個重點整理吧:列舉的四種宣告方式

enum Alpha { X, Y, Z }
const enum Alpha { X, Y, Z }
declare enum Alpha { X, Y, Z }
declare const enum Alpha { X, Y, Z }
  1. 千萬不要 declare 不存在的物件
  2. 使用一般列舉可以產生查找物件,加上const 或 declare 修飾符就無法產生查找物件
  3. 使用 const ,列舉元素只能是常數,且在編譯時會將列舉的元素引用替換成其值
  4. 使用 declare,若沒有賦值該元素會被視為 computed,若有賦值則視為常數

小結

今天探討了兩個 TS 才出現的型別 - 元組(Tuple)和列舉(Enum)。整體而言,元組可視為嚴格的陣列型別,而列舉則是使用在已知資料在一定範圍的情境下,將所有可能一一列出來,使用上各有需要注意的事項,特別是列舉宣告的比較,推薦讀者可以直接看stackoverflow這篇文章喔,明天見啦!

參考資料:
TypeScript 官網
stackoverflow - How do the different enum variants work in TypeScript?
TypeScript enums explained


上一篇
【Day 11】TypeScript 資料型別 - 陣列型別(Array Types)-(下)
下一篇
【Day 13】TypeScript 資料型別 - 字面值型別(Literal Types) & 型別別名(Type Alias)
系列文
Typescript 初心者手札30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

1 則留言

0
kkdayy_55330
iT邦新手 5 級 ‧ 2020-03-17 17:46:32

嗨大大您好,

文中提及 - 列舉又分三種型別,分別為數字列舉(Number enum)、字串列舉(String enum)和異構列舉(Heterogeneous enum)。

那如果分三種型別,另外文中的常數列舉異構列舉不是屬於列舉嗎?還是其實也算是一種列舉延伸?但內文沒有告知清楚呢?謝謝!

我要留言

立即登入留言