閱讀本篇文章前,仔細想想看
- 陣列跟元組(Tuple)的差別在哪裡?什麼時候用陣列 / 元組比較適合呢?
- 什麼時候可能不用對函式的參數進行積極註記?試舉個範例。
如果還沒理解完畢的話,可以先翻看前一篇文章喔!
[2019.09.15 新增] tsconfig.json 設定
這裡筆者必須緊急說明:若讀者試著筆者舉的程式碼範例的話,請記得將裡面的
strictNullCheck
選項改成true
,這一點忘記在文章系列的一開頭提醒讀者,實在是很抱歉!/* tsconfig.json */ { "compilerOptions": { /* ... */ "strictNullChecks": true, /* ... */ } }
因此請讀者注意,目前學習的 TypeScript 型別系統版本多了一個
strictNullCheck
的編譯器屬性設定!至於為何會造成如此狀況,那是因為筆者在專案上習慣將某些 TypeScript 編譯器設定啟動!至於strictNullCheck
到底為何,將會在型別系統講述告一段落後,開始講述 TypeScript 的編譯器設定檔喔![2019.09.18 新增] 程式碼範例
如果想要看到本系列文裡面舉的程式碼範例可以參考 Maxwell-Alexius/Iron-Man-Competition 這個 GitHub Repo 喔~寫作過程當中會不斷更新的!
今天是本系列文章開賽第一週的結尾,筆者都在展示 TypeScript 對於 JS 原始型別或廣義物件的推論註記的行為,讀者可能覺得很多,因此筆者可能會塞一天的文章(但目前不確定會是塞到哪一天)作 TS 推論行為的 Cheatsheet 並註明說:哪個點不懂可以直接看哪一天的文章。(不過依照目前狀況看來可能要把大部分東西整理起來也是得耗費一點時間啦,因此筆者還是鼓勵讀者作筆記,或至少把重點複製到你整理筆記的地方)
另外要跟讀者說明,今天除了講完 TypeScript 的列舉型別(也就是 Enum
,Enumerated Type)後,筆者也要講述剩下的:明文型別(Literal Type)、特殊型別(any
、never
以及 unknown
)的推論與註記;並且也會說明型別化名(Type Alias)以及選用屬性(Optional Properties,也就是讀者有時候看到的 ?
)。這就是下一週的開頭~
不過今天應該聽故事時間(數學申論的東西)也是有點多,所以就~不輕鬆點地讓正文開始吧!
貼心小提示 1. 筆者將會用數學的定義來解釋列舉
本篇章會用數學的正式定義以及一些主觀想法來說明列舉本身的意義,看不懂其實沒關係 —− 會不會用數學對學習 TypeScript 不會有太大的影響,甚至死記列舉的語法或概念也是可以的;用到列舉的狀況,其實也蠻常見的~其中,要判斷什麼時候該用到列舉,也可以用經驗法則的方式學習
讀者可能會問,為何筆者不直接交代語法與用途就好,還要很麻煩地繞一大圈?可能筆者想試試看 Learn TypeScript The Hard Way 這種 Style 或者是能不能突破自我的侷限講出一個完美解。不過重點是:這裡可以想成,筆者單純想推理結合速成法的概念,省掉憑空靠經驗的方法去判斷使用列舉的時機。
若讀者覺得,這對自己時間成本轉換效益不高(意指浪費時間浪費生命),或者是關我 P 事能用就好的話,歡迎直接跳到語法跟範例部分唷!這些東西並不影響讀者會不會使用 TypeScript!但還是強調一點:TypeScript
enum
會在讀者用 TypeScript 開發專案以及後續的文章很常用到。
貼心小提示 2. 為何筆者也不對元組作類似的數學推演
元組使用時機並不多,對這種使用時機低的狀況用複雜的理論說明簡直是要搞死讀者;然而列舉的使用就很常見 —— 能夠適當地判斷什麼時候用列舉,會對用 TypeScript 開發專案的便利性以及可維護性(這是重點)有顯著的提升。
說到列舉(enumerate),它的意義就是將東西一個一個列出來。英文 Enumerate 的說法也是這樣:
enumerate (verb.)
mention (a number of things) one by one.(將一連串的東西一個一個地提及)
由上方的意義可以引申出 —— 列舉成立的條件以及意義:
重點 1. 列舉的意義 Enumerated Type
若有一系列的資料集合 S:
- S 是集合 —— 數學意義上具有確定性、互異性以及無序性。
- 互異性:元素互不重複
- 無序性:元素沒有排列順序
- 確定性:家庭可以用集合表示為有生兒子,有生女兒,亦或者兩性都可能有生又或者是頂客族(根本不生),我們確定只會有四種狀況,但不會生出 1/2 個女兒或 3.1415926 個兒子
- 由於每筆屬於 S 的資料具有互異性,換句話說,集合裡的元素本身就具備獨有特質
- 若這些互異性的元素(或者是資料),對人的主觀來說擁有強烈共通特質的話(比如顏色有紅、黃、藍、綠等顏色,都是指顏色本身),則我們稱此集合的資料具有主觀上強烈的共通性(主觀感覺上,沒有資料共通性的集合,比如:假日會做的事情,每個人基本上會不一樣,集合起來也會天差地遠,範疇過廣,主觀感覺上相關性就比較差;又或者是故意把所有雜項塞在一起,其主觀感覺上的關聯性就比較微弱)
而列舉的意義就是符合以上三點,簡而言之,就是一種數學意義上的集合結合主觀認為具備強烈共通特質
聽起來很像廢話,一大堆繞口令(翻白眼)。不過呢~了解這些東西除了對於理解 TypeScript 語法上有幫助外,還會幫讀者辨別出使用列舉的適當時機!
陣列就違反數學意義上集合的性質:互異性 —— 裡面的元素值不可重複!因此遇到元素可能重複的案例,不能用列舉,選擇陣列比較適合。
光是這一點,我們可能會反駁:譬如可以存一系列的數值代表每年某間公司的盈餘,這些數值的代表意義都是『公司的盈餘』這個概念(共通性成立)。但資料裡可能兩兩比對盈餘數字會有究極微小機率會剛好相等(重複性,違反集合的互異性)。但就算是這種極小機率,用列舉的意義事實上並不大,每個數字本身就違反了集合的確定性 —— 每年盈餘的狀況有無限多種,沒有辦法確定會有什麼樣的盈餘,搞不好也會虧損。
沒辦法控制到,譬如:明年的績效一定要等於 1000 萬元(不然真的是屈就現實而選擇讓集合確定性成立,這也是很偉大的舉動),通常一定會設定至少要超越 1000 萬元或者是比去年還要再高,當然是賺越多、發大財越好啊!是不是!
元組則違反了集合的定義裡所謂的無序性 —— 元組裡的值必須在一定排列下才會顯得有意義。光是這點就可以判斷需不需要用到列舉,儘管元組存的確實是具備強烈主觀共通性的資料,比如前一篇文章所舉的例子:
這些資料的共通點都是在敘述車輛資料。然而,反過來說明,如果故意踢除這個元組的有序性,讓元組裡的值符合集合裡的無序性概念(注意不是元組本身,因為元組不是集合),單純看這四筆資料:
可能因為還停留在剛剛範例的印象,會想到:BMW 出產的重型機車是銀色,其出廠日期為 2019 年 3 月 17 日。
那換個順序來看(按照集合的定義,順序性沒差,因此換了照理來說也不會有邏輯錯誤):
什麼叫做『 銀色的 2019 年 3 月 17 日,BMW 出產了重型機車 』?聽起來邏輯很牽強,怪怪的。開發者也不會選擇用這樣的方式折磨自己的理解能力。(不過有一個例外:我在『 黑色星期五,買下 BMW 出產的重型機車 』;哎!真希望筆者的夢可以趕快圓一圓啊...,話說此例外也是必須在認定資料合理的前提之下才能說可以的呢!)
[2019.09.19 PM 09:07 新增] 貼心小提示
注意
Date
類別的物件在 JS 的陷阱,筆者被搞超多次搞到心寒,就連寫作本篇文也都出意外,幸好及時發現。
Date
的月份部分,通常以為是填1
到12
數字代表一月到十二月,但是Date
的月份起始點是0
開始 -- 代表一月。也就是說new Date(2019, 2, 17)
代表的是 2019 年 3 月 17 日,依此類推。因此本篇文也進行了更正~
儘管無序性被滿足了,但卻犧牲了排序過後(整理過後)的資料而帶來的主觀強烈感覺到的共通性。兩者推論結果都是死胡同,因此可以用一句話形容剛剛的狀況 —— 資料若需經過排序才能產生主觀認為的共通性,可以選擇使用元組,比如說:數學上位於平面上的點座標,通常認定第一個值代表 X,第二個值代表 Y,難道我們會想要故意顛倒這兩個代表的座標嗎?(不過 JSON 物件格式通常可以取代元組,也不需要管順序就間接解決這類型問題,差別在於,JSON 物件不是集合,因此也不是列舉)
讀者可能會問:主觀認定資料的關聯性,到底確切的定義是要多強烈才會值得你用列舉?
這邊的意思只要讀者認為目前的資料都擁有關聯性,符合這一點就可以囉。筆者再用更實際一點例子:
交通方式:比如,騎摩托車、開車、騎腳踏車、步行、坐公車、坐火車、搭船、搭飛機。這些都是各自獨立(互不重複以及順序都沒差)的資料表現形式,主觀也認為這些資料的共通點都是“交通的方式”,除了可以選擇用陣列來表示,更適當的方式是用列舉來代表這些資料型態(因此 Google Map 上面的路線規劃,裡面有分『步行』、『開車』以及『公車』等等的交通方式,就可以使用列舉型別呢)
一個星期總共有 7 天,人類統一了時間曆法規定,禮拜天是 Sunday、禮拜一是 Monday ... 等等。星期裡的每一天不重複(互相獨立);主觀來看,也都在描述一個星期裡的第幾天。(下面的文章以這個 Case 舉例)
檔案的權限:在 Linux 系統裡面常見的三種模式分別為讀取、寫入以及執行 —— 三種模式組合(Combination)起來總共有 C3取0 + C3取1 + C3取2 + C3取3 = 1 + 3 + 3 + 1 = 8
種組合。(高中時期最害怕的排組機...)忘記數學的話,可以列出全部狀況:
讀者評估過後,主觀認為開發時使用的資料,除了不重複外,都有相關聯的話,理論上都是可以使用列舉的時機。(注意『 理論上 』這三個字)
回過頭來,試想一下,剛剛舉出的第三個範例(檔案權限)應用實務上,我們不會刻意將每一種檔案權限等等,各自將所有組合(Combination)用列舉的方式一一舉出來。筆者再舉一下類似的案例:
精密顏色解析:某系統提供 16 個顏色選項給使用者,這時可以運用列舉勉強把這 16 色的代表值一一列舉出來;但現代的科技技術進步,使得我們可以將 16 色擴展為三元色(紅、綠、藍)分別 256 階層的色彩解析,這種 256 * 256 * 256 多達 16,777,216 種色彩組合,用人腦可想而知根本沒辦法一一列舉完畢。不過根據理論,每種顏色值具備不重複以及確定性值,主觀認定都是在描述顏色,排列順序也沒差,是列舉可以表示的形式,只是在時間複雜度太高,因此不採用。
車票組合:台北捷運站光是站數多達 100 站以上。假設總共有 100 個站點 —— 車票組合也高達 100 * 99 = 9,900 種組合。儘管車票具備不重複以及確定性,主觀認定都是車票,在排列上也沒差別 —— 是列舉也可以表示的形式,但實踐的時間複雜度過高,因此不採用。再者,以北捷繼續擴張的案例,勢必又得再新增更多車票種類,非常麻煩。
電影類型:看電影時,不外乎會認為電影可能會分成動作、愛情、推理、恐怖、Sci-Fi 等眾多類型。然而,真要針對電影類型作細分的話,比如蒸氣龐克風、末世風、波希米亞風、音樂劇場等等 —— 可以無限擴張,亦或者是推理懸疑恐怖片、末世蒸氣龐克風、愛情動作片 —— 又是可以互相組合的資料。若嚴格定義:愛情動作片跟純愛情片互相獨立,則資料除了不可重複以及風格確定外,排列這些影片類型也絲毫沒有意義(具無序性),因此推斷影片類型可以用列舉的方式呈現,但是時間效益上根本不高,因為資料會擴張、也可以互相組合。
敏銳的讀者可以猜到,理論上跟實務上使用列舉(Enumerated Type)的時機了吧。
重點 2. 實務上應用列舉的時機
- 單純已經符合使用列舉的標準:
- 資料互不重複,符合資料獨有性
- 資料排序上沒太大意義,符合資料無序性
- 實務上使用列舉的時機,除了符合標準外,其資料形式沒有這些狀況:
- 自組型擴張:資料可以運用原本的資料組合進行擴充(檔案權限可分:讀取、寫入以及執行,但是這三種模式可以擴張成共 8 種情形)
- 應用型擴張:資料在未來的變動機率大,進行擴充性的機會也相對就高(車站站線有很大機率會擴建,造成資料可被列舉種類擴大)
enum
大概知道列舉的性質,我們就回歸現實,看一點比較實際的用法!
剛剛說過 —— 列舉本身可以是數學意義上的集合,且集合裡面的元素摻雜了人主觀認為裡面資料具有共通點。而剛好,TypeScript 列舉的語法跟數學上的集合表示方式非常像,這邊就用一個星期的每一天進行列舉:
當然,你也可以壓縮成一行:
不覺得很像數學上集合的寫法嗎?都是用大括弧表示(沒看過數學上的集合也沒關係,記好語法就可以了)。但是請注意,定義 TypeScript 列舉時,不需要等號!(語法錯誤如圖一)
圖一:TypeScript 直截了當地跟你說,下個要看到的東西是 {
而不是 =
使用列舉時,可以用類似物件呼叫屬性的方式來表達(型別推論結果如圖二):
圖二:很明顯地,TS 直接判斷該變數的型態為列舉
TypeScript 很細心地知道,這個變數的型態就是剛剛所定義的 WeekDay
列舉型態。
當然,列舉的型別註記一如往常地簡單:
讀者會覺得:“又是數學,Oh God”(請不要隨意句點神 XD),但是這裡不會講太多數學。若某函數 g(x) 為 f(x) 的反函數
,則:
g(f(x)) = f(g(x)) = x
意思是:如果知道今天是星期三 Wednesday,根據 WeekDay
推論,今天就是這一週的第 4 天(若禮拜天視為一週的第一天)。
另一種說法是:如果知道今天是整個禮拜的最後一天(也就是第 7 天),則可以推論今天是 WeekDay
中的星期六 Saturday。
以上兩個說法互相相反,但是用繞口令的形式來說:
如果知道今天是(WeekDay 中的)星期三 Wednesday,就可以推得今天是一週的第 4 天,因此又可以反推回今天是 (WeekDay 中的)星期三 Wednesday
簡化說法則是:
(WeekDay 中的)星期三就是(WeekDay 中的)星期三 => 星期三就是星期三(眼睛瞪大)
(這簡化說法搞啥啊!?)
請讀者保持這個感覺,檢視一下 TypeScript 針對 enum WeekDay
編譯出來的結果。
還記得如何編譯 TypeScript 檔案嗎?只要一個 tsc
指令就可以把 .ts
檔案都編譯完成。請注意:你必須要到有 .ts
檔案的資料夾位置去下指令。(相信讀者讀到這一天早就已經熟悉流程,因此不再多做說明)
打開 VSCode 過後的結果出來看(如圖三,index.js
檔案的內容;程式碼如圖四):
圖三:編譯完畢後應該出現的結果圖
圖四:編譯結果
筆者當初學到這裡,第一次看到這種形式,想說怎麼會這麼奇怪?單看編譯過後的 enum
裡面的格式並省略一些重複部分:
熟悉原生 JS 的讀者就會知道 —— 這是 IIFE (Immediately Invoked Function Expression)的格式(補充資料會貼在小結)—— 匿名函式通常會被使用的一種方式。而原生 JS 詮釋出來的 TypeScript enum
就是長這副德性(一副好邪惡的樣子啊)。尤其這一行真是複雜:
但你不覺得格式跟正反函式互相抵銷的模式很像嗎?
筆者將裡面的 WeekDay["Sunday"] = 0
特別拆出來看:
也就是說 WeekDay
本身是 JS 物件,擁有 "Sunday"
屬性對應 0
這個數值。而另一方面:
以上那段可以想成:
也就是說 WeekDay
也擁有數字 0
這個屬性,其對應值為 "Sunday"
。因此可以得知:
當然,這樣不是數學上正式的函式定義(輸入名稱為參數),不過這裡是用 JS 物件的方式去對屬性作相互對應的方式,因此也被筆者稱作為具備反射性。
因此我們可以這樣做(編譯以下的程式碼並且用 node
執行結果如圖五):
圖五:列舉的反射性
不過這裡要注意一個小細節:順向的結果 —— 儘管實際上是數字 5
,但是推斷結果為 Enum
型別,而非 number
(圖六);但是逆向取回列舉的鍵(Key)的結果是字串型別 string
(圖七)。
圖六:順向使用列舉,推論結果為列舉型別
圖七:逆向使用列舉,推論結果為字串型別
重點 3. TypeScript 列舉
- 列舉可以藉由 TypeScript 的
enum
關鍵字進行定義。若我們想定義列舉型別E
,其內含的元素為V1
,V2
...Vn
,則:enum E { V1, V2, V3, ..., Vn }
- 定義列舉型別後,使用該列舉的值並代入到變數時,TypeScript 對於該變數的型別推論是
enum
型別- 定義列舉型別後,可直接使用列舉的名稱作為型別註記
- 列舉具有反射性,所以可以藉由列舉呼叫元素出來的結果反查該元素本身的名稱
- 列舉的潛規則:
- 列舉可以被當成 JSON 物件看待(比如說也可以用
for...in...
迴圈迭代列舉的元素);但與 JSON 物件的差別在於,使用列舉會獨立為enum
型別,而 JSON 物件本身就是一種物件型別- 列舉裡的元素,每一個對應值是從數字
0
開始,每列一個元素會遞增上去- 列舉裡的元素可以自訂對應的數字,後續會一直不停地遞增上去
- 列舉裡的元素可以自訂對應的字串,但是必須接續訂立對應的字串值下去,或者是再返回定義對應值為數字型別
- 可以使用列舉裡定義過後的值進行後續自訂對應值的運算
這裡筆者下的重點中的最後一個:列舉的潛規則部分,筆者考慮打算不放入系列文,一方面筆者個人經驗上很少用到,另一方面是文章實在太大,如果是講解這些細節用法部分,可以參考 TypeScript 官方文件 以及一些跟 TypeScript enum
相關的細節文章會比較適合。
除了基礎的列舉語法外,能夠分辨什麼時機使用陣列、元組以及列舉 是本篇的重點,但如果需要用到更進階的語法,推薦讀者可以直接上網閱讀官方的文件喔!
筆者就緊接著下一篇 —— 明文型別篇章繼續下去~
IIFE 補充資料:重新認識 JavaScript: Day 18 Callback Function 與 IIFE
我是建議從結構型態與常數列舉的角度來看比較單純。
Tuple 的目的是,將元素的型態及順序構成一種結構型態,因為型態與順序固定了,同一型態的 Tuple,就能以可預期的模式,來解構其中的元素。
例如:
type Vector = [number, number]
function move(pt: Vector, amt: Vector): Vector {
let [px, py] = pt;
let [ax, ay] = amt;
return [px + ax, py + ay];
}
console.log(move([0, 0], [5, 10]));
console.log(move([10, 0], [3, 7]));
對 move
來說,預期收到的引數,必然要是 Vector
型態,因此可以透過解構來取得其中的元素。那麼你說,為什麼不用物件?也可以,只不過相對而言,Tuple 比較「便宜」,若是簡單的結構,不需要將特性(property)名稱也作為結構之一來考量時,Tuple 就很方便了。
另外,在 Python 中也有 Tuple,而且是不可變動,在 TypeScript 中如果想進一步也將 Tuple 變成不可變動,可以使用 ReadOnly
:
type Tuple = Readonly<[number, string]>
let employee: Tuple = [1, "Steve"];
employee[0] = 10; // Error
Day3 的〈Day 03. 物件型別 X 完整性理論 - Object Types Basics〉中談到,可以用元素的名稱、型態來定義一種結構型態,那有沒有同時考量元素的「名稱、型態及順序」的?目前看來,TypeScript 沒有(雖然 TypeScript 4.0 有 labeled tuple,不過就只是名稱標示),不過其他語言中有,像是 Java 目前已有的 record
類別,或 Haskell 的 data
,就 Java 的 record
而言,還可以進一步列舉 record
類別的子型態,用於想控制子型態,不隨便讓 API 客戶端擴充的場合。
列舉?是的!其實能列舉的對象,不只是常數,也可以列舉型態。
不過就 TypeScript 的設計而言,就真的只是用來「列舉一組常數」,這些常數指示函式,接下來要對應於哪種狀態,進行特定的處理,列舉常數是有限的,因此這意謂在某個型態下的狀態是有限的,例如:
enum Action {UP, DOWN, LEFT, RIGHT};
function doAction(action: Action) {
switch(action) {
case Action.UP:
console.log('上');
break;
case Action.DOWN:
console.log('下');
break;
case Action.LEFT:
console.log('左');
break;
case Action.RIGHT:
console.log('右');
break;
defult:
console.log('XD');
}
}
doAction(Action.DOWN);
在上面的例子裡,Action
型態就只有四個實例,意謂著可指示四種狀態,因為列舉具備型態,doAction
只能接受 Action
實例,也就是那四個值(如果是在 Java 裡,case
沒列全或多列,都會引發編譯錯誤,不過 TypeScript 似乎不 care,因此會需要 default
)。