今天要來介紹一下 TS 有而 JS 沒有的資料型別 —— 元組(Tuple)和列舉(Enum)
元組可以想成是一個嚴格的陣列,陣列的元素是固定數量的,且每個元素的型別都是已知的,但型別不用相同。
還記得怎麼進行陣列的型別註記嘛? 例如:想要創建一個可以含有數字和字串的陣列可以這樣寫
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 跟 Tuple 一樣是 TS 增加的新語法,也被稱做「列舉」或「枚舉」,Enum 可以想成就是將東西一個一個列出來,通常會用它來管理多個同系列的常數(不可修改的變數),將常數值一一列舉出來,且只能在列舉出來的範圍中選擇,例如:交通號誌顏色的列舉,元素就是是紅、綠、黃三個。列舉又分三種型別,分別為數字列舉(Number enum)
、字串列舉(String enum)
和異構列舉(Heterogeneous 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}
結果分別為:
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)。
剛剛我們提到的都是數字列舉的部分,來看一下字串列舉
。字串列舉的概念很簡單,在字串列舉中,每個元素都必須賦值字串資料來進行初始化:
enum Direction {
Up = "UP",
Down = "DOWN",
Left = "LEFT",
Right = "RIGHT",
}
編譯結果如下:
倘若沒有賦值,就會報錯 ; 如果字串相同,跟數字列舉一樣,TS 是不會檢查到的,因此,要特別小心。另外,要注意的是字串列舉沒有反向映射。
問題來了,那字串和數字元素可否在同一個列舉中呢?答案是可以的,這就是我們還沒提到的「異構列舉」(Heterogeneous enum)。
舉例來說:
enum Direction {
Up = "UP",
Down = "DOWN",
Left =3,
Right ,
}
編譯出來的結果如下:
但雖然技術上來說是可以的,但官網並不建議使用。
在 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.
常數列舉和上面的普通列舉最大的差異就是
倘若想保留常數列舉,也可以在 tsconfig.json 檔設定 preserveConstEnums
為 true
。
外部列舉是用來描述已經存在列舉型別的輪廓,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 }
- 千萬不要 declare 不存在的物件
- 使用一般列舉可以產生查找物件,加上const 或 declare 修飾符就無法產生查找物件
- 使用 const ,列舉元素只能是常數,且在編譯時會將列舉的元素引用替換成其值
- 使用 declare,若沒有賦值該元素會被視為 computed,若有賦值則視為常數
今天探討了兩個 TS 才出現的型別 - 元組(Tuple)和列舉(Enum)。整體而言,元組可視為嚴格的陣列型別,而列舉則是使用在已知資料在一定範圍的情境下,將所有可能一一列出來,使用上各有需要注意的事項,特別是列舉宣告的比較,推薦讀者可以直接看stackoverflow這篇文章喔,明天見啦!
參考資料:
TypeScript 官網
stackoverflow - How do the different enum variants work in TypeScript?
TypeScript enums explained
嗨大大您好,
文中提及 - 列舉又分三種型別,分別為數字列舉(Number enum)、字串列舉(String enum)和異構列舉(Heterogeneous enum)。
那如果分三種型別,另外文中的常數列舉、異構列舉不是屬於列舉嗎?還是其實也算是一種列舉延伸?但內文沒有告知清楚呢?謝謝!