iT邦幫忙

第 11 屆 iT 邦幫忙鐵人賽

1
Modern Web

讓 TypeScript 成為你全端開發的 ACE!系列 第 43

Day 43. 通用武裝・泛型註記 X 推論未來 - TypeScript Generic Declaration & Annotation

https://ithelp.ithome.com.tw/upload/images/20191009/20120614YE06rKGIDj.png

閱讀本篇文章前,仔細想想看

  1. 泛用型別的意義是什麼?
  2. 泛用型別大致上有哪些種類或形式?

如果還不清楚可以看一下前一篇文章喔~

以下就直接正文開始

泛用化名的宣告與註記機制 Generic Type Alias Declaration & Annotation

讀者可能想問:“這不是前一篇講過了嗎?”

筆者必須要說,泛型要討論的案例比較多,而且上一篇只是介紹而已,以下就才是真正地進入正式討論

泛用化名 Generic Type Alias

貼心小提示

讀者可能覺得這標題很疑惑:“為何不要直接取泛用型別,還要取泛用化名?”

筆者認為,如果要很精確討論泛用的行為,而又要概括型別(Type)、介面(Interface)與類別(Class)的狀況,因為共通點都是型別化名的一種,所以才會取這個名稱

首先,筆者就以 Array<T> 這個泛用型別作為基礎。

https://ithelp.ithome.com.tw/upload/images/20191009/20120614kxYvkWhrmI.png

讀者有沒有想過,如果泛用型別沒有指定型別參數的值會發生什麼事情

以上面的程式碼為例,unspecifiedTypeParamArr 儘管被註記為 Array 型別,但卻沒有指名 Array<T> 裡面的 T 型別參數。(推論結果如圖一;錯誤訊息如圖二)

https://ithelp.ithome.com.tw/upload/images/20191009/2012061418C4yPo5Qi.png
圖一:被 TypeScript 警告,Array 這個型別註記上很有問題。

https://ithelp.ithome.com.tw/upload/images/20191009/20120614c4o0SgrBfb.png
圖二:TypeScript 告訴你,Array 不單單只是 Array 型別,它的形式是泛用的 —— 是為 Array<T>;也就是說,你如果少掉指名型別參數 T 的值就會出錯。

重點 1. 泛用化名的型別參數 Type Parameter(s) of Generic Types

除非型別參數有預設值(又可稱作預設型別參數 Default Type Parameter),否則本身為泛用形式的型別化名,少掉任何一個型別參數的值就會出錯

泛用化名宣告 Declaration of Generic Type Alias

既然提到了預設型別(Default Type Parameter)—— 但在講這個 Feature 之前,筆者還是很雞婆地先把宣告泛用型別化名的方式提點一下。

重點 2. 泛用化名的宣告 Declaration of Type Alias

型別化名分成三種 —— 型別(Type)、介面(Interface)與類別(Class)。

若想宣告泛用型別(Generic Type)GT,並且擁有型別參數 TP1TP2 ... TPn,格式如下:

https://ithelp.ithome.com.tw/upload/images/20191009/20120614bIOeWZ9q9h.png

若想宣告泛用介面(Generic Interface)GI,並且擁有型別參數 TP1TP2 ... TPn,格式如下:

https://ithelp.ithome.com.tw/upload/images/20191009/20120614mgSGkufuep.png

若想宣告泛用類別(Generic Class)GC,並且擁有型別參數 TP1TP2 ... TPn,格式如下:

https://ithelp.ithome.com.tw/upload/images/20191009/20120614gj7wdzqzlu.png

泛用抽象類別就是在泛用類別旁邊註記 abstract 關鍵字就可以了。

不過,宣告的泛用化名裡面,沒有使用到宣告的型別參數,預設的 TypeScript 編譯器設定不會出現任何錯誤訊息,但還是建議讀者不要這樣亂宣告型別參數卻沒有使用

除非你怕有闕漏,可以開啟 tsconfig.json 裡面的 noUnusedParameter 選項,它就會出現 Declared but never used 類似這樣的警告訊息。(如圖三)

https://ithelp.ithome.com.tw/upload/images/20191009/20120614nkggki2O3S.png
圖三:noUnusedParameter 預設為 false,開啟它就會出現 Declared but never used 警告喔~

預設型別參數 Default Type Parameter

預設型別的語法其實很簡單,就是用等號連結型別值就夠了,跟 ES6 介紹的函式預設參數(Default Parameters)的概念差不多。

https://ithelp.ithome.com.tw/upload/images/20191009/20120614P0AfTA6EXp.png

以上的推論狀況就由讀者自行驗證吧,基本上都不會出錯的~

重點 3. 預設的泛用化名型別參數 Default Type Parameter

若某型別化名 Tspecified 不為泛用型別,而泛用型別 GT 的宣告為:

https://ithelp.ithome.com.tw/upload/images/20191009/20120614LDbv9xhIpm.png

則我們稱:Tspecified 為型別參數 TP 的預設型別(Default Type)。

若在變數註記上註記 GT 型別卻沒有指名 TP 代表的型別值,則該 GT 型別的註記等效於 GT<Tspecified> 的註記行為。

https://ithelp.ithome.com.tw/upload/images/20191009/20120614BaqumqnhCP.png

同樣的情形也適用於介面(Interface)或類別(Class)的型別化名宣告

泛用化名之型別參數限制 Type Constraint

另外,讀者可能也會覺得,泛用化名的使用有時候並不需要自由到任何型別都可以泛用

以下就舉一個型別參數限制(Type Constraint)的案例。

https://ithelp.ithome.com.tw/upload/images/20191009/20120614fCu2TIuo7a.png

以上的程式碼宣告 Primitives原始型別的 union 複合的結果,這應該對看到這裡的讀者很熟悉。

另外 PrimitiveArray 的宣告裡出現了 extends 關鍵字 —— 型別參數裡面若出現 extends 就代表該參數被限制到某個範圍 —— 這就是型別參數限制的用法。而 PrimitiveArray 的型別參數 T 被限制為 Primitives 型別可以囊括的範圍。

以下的範例程式碼簡單地進行測試。(檢驗結果如圖四;錯誤訊息如圖五)

https://ithelp.ithome.com.tw/upload/images/20191009/201206148o4a9AgNZr.png

https://ithelp.ithome.com.tw/upload/images/20191009/20120614ds2cdKRxqc.png
圖四:很明顯 —— PrimitiveArray<PersonalInfo> 被警告了

https://ithelp.ithome.com.tw/upload/images/20191009/20120614qNNm0b7hHj.png
圖五:PersonalInfo 沒有符合 Primitives 的限制需求,因此被 TypeScript 駁回

值得注意的點是,從以上的案例 —— string | number 這種複合型別也符合 Primitives 的範疇,也就代表只要複合過的結果也包含在 Primitives 就會予以通過呢

重點 4. 泛用型別化名的型別參數限制 Type Constraint in Generic Type Alias

某泛用型別化名 GT 之型別參數 Tparam 被限制在某型別 Tcontraint 範圍之下,可以使用 extends 關鍵字,其格式為:

https://ithelp.ithome.com.tw/upload/images/20191009/20120614mty6pWjsCK.png

則該型別 GT 一但被註記到某變數時,裡面的型別參數不能代入超出 Tconstraint 以外的型別值。

Tconstraint 為一系列型別 T1T2、...、Tnunion 結果,則 Tparam 代入的型別可以為 T1T2、...、Tn 任意組合 union 過後的結果。

泛用介面與泛用類別的型別參數限制的語法皆相同。

讀者試試看

  1. 若是出現 intersection 的狀況,型別的限制行為為何呢?讀者也可以想想看為何不太需要討論到 intersection 的型別限制狀況。(提示:某些 intersection 狀況會導出 never 型別,亦或者是 intersection 過後的結果不太需要使用參數型別的限制;所以才說第一篇章《前線維護》很重要吧~)

https://ithelp.ithome.com.tw/upload/images/20191009/20120614b5ERQlVEww.png

  1. 回歸 union 的情境,如果以上的範例將 User 這個泛用型別的參數限制改成 union 的形式,參數型別限制的狀況會如何?

https://ithelp.ithome.com.tw/upload/images/20191009/20120614bxy0DWNVnH.png

不過,讀者可能又會問 —— 有沒有單純限制型別參數範疇為 Primitives 中的其中一種型別而非複合過後的型別呢

恩... 有的 XD,但這要用到條件型別的寫法,這邊筆者暫時展示一下但不會多作探討。(如果筆者認為真的有必要會深究下去,啊~~~好想要探究下去

https://ithelp.ithome.com.tw/upload/images/20191009/20120614zzhr4TCmXo.png

以上的程式碼推論結果如圖六、錯誤訊息如圖七。

https://ithelp.ithome.com.tw/upload/images/20191009/20120614m671vYqoil.png
圖六:自然而然,number | string 是不能被 TypedPrimitiveArray 混用狀態

https://ithelp.ithome.com.tw/upload/images/20191009/20120614Q5ZjF76Dap.png
圖七:不過這個錯誤訊息很奇怪,而且 invalidPrimitiveUnionedArr 被推論的結果是 number[] | string[]

筆者認為目前沒有必要講以上的程式碼的機制,因為超出本日內容範疇;然而,筆者倒是可以劇透一下:

條件型別(Conditional Type)針對 union 過後的複合型別具有分配性質(Distributive Property),跟數學上的分配律很像。

詳細可以查詢官方對於條件型別的描述內容

貼心小提示

如果讀者看到 never 這個型別還沒領會它的真諦,請參見本系列探討 never 型別的篇章

很重要喔!如果你只認為 never 就只有 Handle Error 的情形,但你不知道 never 背後代表的深層意義,誠心建議真的要再回頭看看本系列文章

泛用函式 Generic Functions

泛用的函式,筆者特別跟其他泛用型別化名隔離掉的最主要原因也是變化性特多,相信看過上一篇介紹泛型的讀者應該也會知道 —— 泛用的函式型別之函式的參數可以註記為泛用函式所宣告的型別參數。(這句話真繞口令

不想看繞口令,請看看以下範例:

https://ithelp.ithome.com.tw/upload/images/20191009/20120614boBEsGyWYV.png

以上面的程式碼為範例,traverseElements 擁有一個型別參數 —— 該函式的目的是要遍歷一個陣列:

  • 第一個參數 values 對應型別為 Array<T>
  • 第二個參數為一個回呼函式,型別為 (el: T, index: number) => void

假設我們想要遍歷一個數字型別的陣列,於是可以這樣做:

https://ithelp.ithome.com.tw/upload/images/20191009/20120614zfiPQylPyj.png

另外,我們當然可以簡化上面的程式碼為:

https://ithelp.ithome.com.tw/upload/images/20191009/20120614w4xOIGYPO2.png

看起來已經簡化到一個境界,但實際上我們還可以再簡化成:

https://ithelp.ithome.com.tw/upload/images/20191009/20120614xD0AyEDx8R.png

讀者應該會發現,筆者把除了 traverseElements 的泛用型別參數的註記外,其他的註記都刪掉了

問題是,筆者在很久很久以前的函式型別篇章有明確講到:

若宣告一個函式的時候,該函式的參數必須要進行註記,不然會無緣無故被 TypeScript 判定為 any 型別

不過!筆者也提到:

某些情況下,我們不需要對函式的參數進行積極註記的動作(參見陣列與函式篇章

其中,最大的原因是因為 —— 泛用型別的參數被指定後,函式內部的參數型別推論也跟著被固定

讀者請以 TypeScript 編譯器的角度試想:

今天有人告訴你 traverseElements 的定義如上面的範例程式碼所示,另外又有人告訴你 traverseElements 使用時將該型別參數 T 註記為 number 型別,是不是也就等同於 —— values 這個 traverseElements 的參數之型別被鎖定為 Array<number> 以及 callback 這個參數的型別被鎖定為 (el: number, index: number): void

也就是說,就算 callback 裡面的參數代入的是:

https://ithelp.ithome.com.tw/upload/images/20191009/20120614b2GOooL3Sz.png

這種參數沒有被註記過後的函式,只要你看到 traverseElements 之型別參數被代入 number,你就可以間接推論該回呼函式的參數型別呢?(callback 函式之參數型別推論結果如圖八)

https://ithelp.ithome.com.tw/upload/images/20191009/20120614FhpJW4KLjb.png
圖八:就算 callback 函式裡面的參數沒有被註記,el 這個參數也會被自動推論為 number

這也是通常 Array.prototype 系列的方法,裡面只要有包含需要代入回呼函式的情形,通常也不需要註記回呼函式的參數之型別:

早在你宣告一個陣列時,藉由隱藏在陣列裡的泛用機制,編譯器早就可以推論出回呼函式的參數型別

https://ithelp.ithome.com.tw/upload/images/20191009/20120614fBeBpEoDXX.png
圖九:泛用型別的妙處在於,編譯器可以直接預測你必須要填入的參數之型別,以 Array<string> 型別的值為例,使用 Array.prototype.map 方法就會拋出 callbackFn: (value: string ...) => unknown 這個提示性說明。

https://ithelp.ithome.com.tw/upload/images/20191010/201206148fvu1LJb6q.png
圖十:只要將 Array<T> 轉換成另類型別,以這個例子來看,Array<number> 型別的值使用 Array.prototype.map 方法的提示性說明變換成 callbackFn: (value: number ...) => unknown,彈性真的很高。

所以筆者得出一個結論:泛用型別可以推論未來的型別使用的可能性,這也就是為何讀者不需要每一次都要進行積極註記的動作,有時候泛用型別的機制就可以讓 TypeScript 推論出正確的結果喔!

重點 5. 泛用函式的宣告 Declaration of Generic Functions

泛用的函式宣告 —— 必須要將型別參數馬上宣告在函式名稱的後面。若某函式所擁有的型別參數有 TP1TP2、...、TPn,則:

https://ithelp.ithome.com.tw/upload/images/20191009/20120614GZmzlf3eS3.png

另外,函式的參數以及輸出型別可以為泛用函式所宣告的型別參數或型別參數的各種組合:

https://ithelp.ithome.com.tw/upload/images/20191009/201206143mpqNbgvPv.png

重點 6. 泛用型別參數與型別推論的關聯 Relation Between Generic Type Parameter & Type Inference

泛用的型別參數註記最大的用途在於 —— 可以預測未來的程式碼之型別推論的可能性

重點 6 短短的一句話就是使用泛用型別的最大好處,這也是為何寫 TypeScript 時,你不需要汲汲營營地將每個變數或函式的參數進行積極註記的動作,筆者後面都會來回利用 TypeScript 的泛用型別的這個優勢寫程式碼。

另外,泛用函式之型別參數的宣告也可以使用重點 3 與 4 講過的預設型別(Default Type)與型別限制(Type Constraint),這一點筆者就不再贅述,用法寫法差不多。

小結

這一篇都在打泛用型別的基礎以及一些功能,斤斤計較的地方看起來也很多,不過有了這些基礎,筆者可以開始寫泛用型別的應用囉~


上一篇
Day 42. 通用武裝・泛用型別 X 型別參數化 - TypeScript Generics Introduction
下一篇
Day 44. 通用武裝・介面與類別 X 泛型註記機制 - TypeScript Generic Class & Interface
系列文
讓 TypeScript 成為你全端開發的 ACE!51

尚未有邦友留言

立即登入留言