閱讀本篇文章前,仔細想想看
- 陣列的推論大致上是如何運作呢?
- 什麼時候要積極去對陣列進行型別註記呢?
如果還沒理解完畢的話,可以先翻看前一篇文章喔!
[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 喔~寫作過程當中會不斷更新的!
等等!?我們在Day 04.不就把函式這個廣義物件的一種表現形式之型別推論與註記講完了嗎?
“難.道.作.者.在.耍.我.嗎?”(眉頭深鎖覺得案情不單純)
“我好不容易能夠堅持地把型別推論跟註記學到這裡,難道作者除了這點其他就什麼都不會嗎?”(心好痛痛難道作者都不體諒人嗎)
“還是說作者真的可以把這種東西寫到 30 天呢?搞啥呀!”(覺得崩潰、覺得悲傷、難受到好想哭哭)
別急!請不要退訂閱!筆者跪下來懇請讀者看下去,給筆者重申我們必須 Revisit 函式型別的原因!
還記得函式型別篇章特地埋的一個梗嗎?筆者立馬原封不動地搬過來:
(函式物件的章節之)重點 2. 函式的推論與註記
分別為輸入的參數與輸出的註記部分,大部分情況下,只要提供函式參數的註記,輸出就可以間接被 TypeScript 推論出來
今天就要來揭曉,什麼情況下 —— 甚至不需對函式參數註記,TypeScript 也能夠正確地推論出參數型態!今天就是要來補洞啦!(請不要亂想!)
讀者肯定覺得:“哦~所以只要是回呼函式,我都可以不用在乎函式參數的型別囉?”。
恩... 這樣的想法太天真了,當然不太可能。筆者這邊會舉例說明:通常回呼函式的參數型別是不用被積極註記,TypeScript 就可以準確推論出該參數的型別的案例。(但不是指全部的回呼函式都不需註記啊!)
陣列是很好的例子,比如:
讀者若還記得陣列被 TS 推論的結果,應該曉得 numbers
對應的型別為 number[]
。這裡使用 Array.prototype.map
方法作為範例。
貼心小提示
沒有用過
map
方法的讀者們,這裡為筆者做一點小補充,畢竟筆者都以這個方法舉例了,不能不做任何解釋。
map
、filter
以及reduce
是常見對於處理串列型資料(這裡指陣列)很好用的方法,為什麼呢?首先,如果想要讓
numbers
裡的每一個值都可以乘以 2 —— 通常初心者們會使用for
迴圈(while
也行)去遍歷整個陣列裡的元素,再進行乘以 2 的動作,如以下程式碼:這樣的做法並不是不行,但換個方式想 —— 要是有一台黑盒子代表一個函式
f(x)
,並且只要寫得像是這樣:f(x) = x * 2
,形式變簡單許多。把
f(x) = x * 2
類比為 JS 的函式的話,會變成:function double(num) { return x * 2; }
對於以上處理數值的過程稱之為 Mapping(映射),也可稱作 Transformation(轉換):
讀過線性代數的人,應該會對線性轉換 Linear Transformation 這個詞彙有印象。線性轉換專門是針對數學上的矩陣進行映射(或者就是轉換)的概念,比如說偏移 Translation、旋轉 Rotation 以及縮放 Scale。
在這裡,
Array.prototype.map
是針對陣列裡的值作映射的概念,所以照理來說,我們把映射的函式帶進去,就像是這樣:以上程式碼會將
nums
裡的每一個元素帶入黑盒子的輸入端 —— 也就是function (x)
中的x
參數。然後每個元素映射出來的結果是x * 2
。最後再把所有元素結果匯集起來!因此,短短一行程式碼就可以不用
for
迴圈方式也可以實踐出相同的功效。(如果讀者有興趣,請上網查查函數式編程跟指令式編程的差別,或者打關鍵字 Functional Programming v.s. Imperative Programming)剩下的filter
跟reduce
的原理比較類似,但出來的結果會跟map
有差。由於不在本系列討論範疇,就留給不會的讀者自行去進修囉。
因為這貼心小提示體積太龐大,筆者再把剛剛的範例拽下來:
我們知道 numbers
會被推論為 number[]
型態。再者,TS 可以也很聰明地判斷這些狀況:
(不過 TS 針對呼叫方法時,也會自動幫助我們辨識型別的內部機制 -- 筆者還沒介紹,如果你忘記 TypeScript 具有這種特性,請參考前一篇喔)
對於回呼函式,TypeScript 也很聰明地認為:我們的 map
方法接收到的回呼函式 —— 裡面的參數應該要等於該陣列裡元素的型別。(如圖ㄧ)
圖一:看到了沒~ TS 認得這個回呼函式的輸入型別為數字呢 (parameter) num: number
因此可以確定,並不是所有函式的參數必須積極註記。
但前提是:我們也要懂得什麼時候該進行函式的參數註記。
關於這一點呢,筆者只能笑笑地說:“完全只能靠經驗啦~”,但有跡可循的是:部分回呼函式之所以不需要對參數作註記,是因為有牽扯到後續會講到的 Generics 泛用型別的機制與實作。(對本系列文是好遙遠的未來啊~)
至於有沒有其他的案例?
有的,跟型別化名(Type Alias)的運用有關外,仍然會跟 Generics 泛用型別的概念扯到!而理解為何回呼函式有時候不需要積極註記就必須探討 Generics。
這裡就先下個簡單的重點概要吧。
重點 1. 函式的參數不需被註記的情況
- 回呼函數在某些情況下不需對輸入參數部分進行註記,原因是藉由泛用型別 Generics 的機制,我們可以設計出讓 TypeScript 能夠藉由泛用型別參數所獲取的外部型別資訊,提前預知到未來的程式碼執行的狀況下,對於各種變數、函式的輸入輸出、類別屬性與方法的型別等等 ... 的型別推論。
- 型別化名(Type Alias)的運用在大部分的狀況下也可以取代積極註記的必要性。
大部分初學沒有泛用型別概念的讀者應該是看不懂的狀態(除非你有 Java、C# 等本身也有 Generics Feature 的背景)。這點倒是沒關係,讀者只要先看到第一句話:“回呼函數在某些情況下不需要對輸入參數部分進行註記” —— 點到為止就好!(哎...泛用型別是進階觀念,所以才放到很後面,讀者忍耐一下)
另外,型別化名(Type Alias)會在後續的章節揭曉,不過難度並不高,請放心。
基本上,研究到這裡的讀者,筆者向你道賀 -- 你已經釐清了大部分基本物件(或是筆者講的廣義物件)的型別推論跟註記的機制啦!
雖然我們還差類別(也就是 Class)部分的推論註記機制還沒介紹,但這個部分就留待第二篇章《機動藍圖》一併介紹下去囉~
好的,接下來介紹一下 TypeScript 本身有而 JS 本身沒有的資料型別 ——『 元組 Tuples 』以及『 列舉 Enums 』。
由於元組這東西跟陣列很像,因此筆者打算(再整一下讀者)比較這兩種東西的差異,讓讀者有更全面性的了解。
筆者直接先切入一個重點。
重點 2. 元組值指派行為
當元組值被直接指派到變數時,必進行積極型別註記。
而被指派元組型別的變數也必需進行積極註記。
綜合以上觀點,只要遇到元組必須要進行註記行為。
這舉一個簡單的例子:若想要存取車輛的資訊,假設以 JS 陣列格式存取 —— 第一個值為車輛廠牌、第二個值為車輛類型、第三個為顏色以及最後一個為出廠日期。
讀者可能早就知道,這裡的型別註記一定都會是字串與日期(Date
)物件的 union
後的陣列型別,也就是 (string | Date)[]
。這裡就不貼結果圖讓讀者自行驗證。
為了解決這情形,TypeScript 介紹了原生 JS 本身就沒有的元組型別:假設變數 A
儲存某一元組值,其中元組裡有 N
筆資料,每一筆資料的值為 Vn
,對應到的型別以 Tn
表示,則其註記的方式為:
如果按這個規則代入到剛剛的範例:
讀者看到這裡一定想說:“哇塞,這程式碼麻煩到炸,每一次都必須得重新複製再貼上”。
筆者暫時先引入某處提到的技巧 —— 型別化名(Type Alias)。
型別化名的寫法是用 TypeScript 裡的 type
關鍵字進行型別的宣告,規則如下:
type <custom-type-name> = <your-type>;
當 type
關鍵字出現時,旁邊接上你要建立的型別化名的名稱,等號右邊接上你想放的型別格式,通常用來整理程式碼以及**抽象化(Abstraction)**型別格式用。
因此,我們將剛剛那一長串的元組型別的格式用型別化名進行抽象化的動作,就可以將每一個變數後的那一長串註記取代掉:
這裡再提醒一下:我們還有不同的方式進行型別的註記。(以下程式碼結果如圖二)
圖二:元組型別的註記結果
好的,那麼元組到底幫助我們開發上的哪個層面?
這裡就必須比對元組跟陣列的差異,相信讀者光是從型別註記上就可以看得出一些端倪。以剛剛的車輛範例,我們得知陣列型別跟元組型別的狀態如下:
重點 3. 陣列與元組的差異
型別陣列裡,只要裡面的元素之型別為此陣列規定的範疇內(比如說
(number | string)[]
只能存取數字跟字串),除了沒有限定元素的數量外,順序也不限定;元組型別則是除了元素的個數必須固定外,格式必須完全吻合,因此裡面元素型別的順序也是固定。
也就是說,以下情形一定都會被 TS 警告(結果如圖三):
圖三:各種元組會出錯的狀況
不過元組的缺點就是:型別等同的兩個元素,儘管型別上是正確的,但是資料的意義上是不同的,就算被顛倒,TypeScript 也因為只會比對型別而不會進行警告!
筆者必須強調:如果開發者要使用元組時,通常會建議資料儘量用 JSON 物件格式表示。譬如剛剛的例子就可以把車子表示為:
除了更具有描述性外,也不會具有元組裡型別相同但資料意義順序有差的問題。
再者,如果真要使用元組的話,我們只能在專案上手動註解(Comment)描述這個元組的每一個元素對應的意義是什麼。因為在看程式碼的時候,你的型別推論結果如果是:
[string, string, string, Date]
跟另外一種推論出的結果長這樣:
{
brand: string,
type: string,
color: string,
manufactureDate: Date
}
當然一定是後者具有更多資訊,前者還得猜來猜去,完全搞不清元組裡哪個資料具備之意義為何。分辨元組各個資料上的意義,最好的方式頂多也只是加個備註吧。但很明顯地,TS 不會自動跟你說明每個型別的具備的意義是什麼。
筆者唯有幾個狀況想得到可以使用元組:
- 人類可以直接認知的規則。比如,在人類的認知裡,數學上所描述的點在平面上的座標可以用 X 與 Y 表示,因此可以表示為:
type Point = [number, number];
- 從 CSV 檔案(或者是資料庫 / 陣列型資料)裡讀取之結果: TypeScript 看靜態的程式碼時,一定不知道你的檔案資料長什麼樣子,勢必得經過 NodeJS 執行並且解析出成果才知道資料到底是什麼;但在這之前,由於 TS 真的完全不知曉該資料被解析後的型別,因此會自動推論為
any
—— 為了避免此狀況,這時才會祭出元組型別來剔除any
型別的困擾
元組算不上困難,對我們來說只是不常用罷了。
筆者個人認為,最大的收獲 —— 除了已經講完大部分的物件型別推論外,也展示給讀者在什麼樣的狀況下,函式的參數不必作型別註記。
而元組(Tuple)其實也沒辦法講太多,因此緊接在下一篇把 TypeScript 另一個自定義型別(也就是 Enum
) 結束掉吧!
(本篇筆者內心 OS:元組這個詞實在是有夠機車,每次打字都會變成『元祖』,結果到頭來為了糾正這個錯誤改到心累)