
閱讀本篇文章前,仔細想想看
- 泛用型別的意義是什麼?
- 泛用型別大致上有哪些種類或形式?
如果還不清楚可以看一下前一篇文章喔~
以下就直接正文開始~
讀者可能想問:“這不是前一篇講過了嗎?”
筆者必須要說,泛型要討論的案例比較多,而且上一篇只是介紹而已,以下就才是真正地進入正式討論。
貼心小提示
讀者可能覺得這標題很疑惑:“為何不要直接取泛用型別,還要取泛用化名?”
筆者認為,如果要很精確討論泛用的行為,而又要概括型別(Type)、介面(Interface)與類別(Class)的狀況,因為共通點都是型別化名的一種,所以才會取這個名稱。
首先,筆者就以 Array<T> 這個泛用型別作為基礎。

讀者有沒有想過,如果泛用型別沒有指定型別參數的值會發生什麼事情?
以上面的程式碼為例,unspecifiedTypeParamArr 儘管被註記為 Array 型別,但卻沒有指名 Array<T> 裡面的 T 型別參數。(推論結果如圖一;錯誤訊息如圖二)

圖一:被 TypeScript 警告,Array 這個型別註記上很有問題。

圖二:TypeScript 告訴你,Array 不單單只是 Array 型別,它的形式是泛用的 —— 是為 Array<T>;也就是說,你如果少掉指名型別參數 T 的值就會出錯。
重點 1. 泛用化名的型別參數 Type Parameter(s) of Generic Types
除非型別參數有預設值(又可稱作預設型別參數 Default Type Parameter),否則本身為泛用形式的型別化名,少掉任何一個型別參數的值就會出錯。
既然提到了預設型別(Default Type Parameter)—— 但在講這個 Feature 之前,筆者還是很雞婆地先把宣告泛用型別化名的方式提點一下。
重點 2. 泛用化名的宣告 Declaration of Type Alias
型別化名分成三種 —— 型別(Type)、介面(Interface)與類別(Class)。
若想宣告泛用型別(Generic Type)
GT,並且擁有型別參數TP1、TP2...TPn,格式如下:
若想宣告泛用介面(Generic Interface)
GI,並且擁有型別參數TP1、TP2...TPn,格式如下:
若想宣告泛用類別(Generic Class)
GC,並且擁有型別參數TP1、TP2...TPn,格式如下:
泛用抽象類別就是在泛用類別旁邊註記
abstract關鍵字就可以了。
不過,宣告的泛用化名裡面,沒有使用到宣告的型別參數,預設的 TypeScript 編譯器設定不會出現任何錯誤訊息,但還是建議讀者不要這樣亂宣告型別參數卻沒有使用。
除非你怕有闕漏,可以開啟 tsconfig.json 裡面的 noUnusedParameter 選項,它就會出現 Declared but never used 類似這樣的警告訊息。(如圖三)

圖三:noUnusedParameter 預設為 false,開啟它就會出現 Declared but never used 警告喔~
預設型別的語法其實很簡單,就是用等號連結型別值就夠了,跟 ES6 介紹的函式預設參數(Default Parameters)的概念差不多。

以上的推論狀況就由讀者自行驗證吧,基本上都不會出錯的~
重點 3. 預設的泛用化名型別參數 Default Type Parameter
若某型別化名
Tspecified不為泛用型別,而泛用型別GT的宣告為:
則我們稱:
Tspecified為型別參數TP的預設型別(Default Type)。若在變數註記上註記
GT型別卻沒有指名TP代表的型別值,則該GT型別的註記等效於GT<Tspecified>的註記行為。
同樣的情形也適用於介面(Interface)或類別(Class)的型別化名宣告。
另外,讀者可能也會覺得,泛用化名的使用有時候並不需要自由到任何型別都可以泛用。
以下就舉一個型別參數限制(Type Constraint)的案例。

以上的程式碼宣告 Primitives 為原始型別的 union 複合的結果,這應該對看到這裡的讀者很熟悉。
另外 PrimitiveArray 的宣告裡出現了 extends 關鍵字 —— 型別參數裡面若出現 extends 就代表該參數被限制到某個範圍 —— 這就是型別參數限制的用法。而 PrimitiveArray 的型別參數 T 被限制為 Primitives 型別可以囊括的範圍。
以下的範例程式碼簡單地進行測試。(檢驗結果如圖四;錯誤訊息如圖五)


圖四:很明顯 —— PrimitiveArray<PersonalInfo> 被警告了

圖五:PersonalInfo 沒有符合 Primitives 的限制需求,因此被 TypeScript 駁回
值得注意的點是,從以上的案例 —— string | number 這種複合型別也符合 Primitives 的範疇,也就代表只要複合過的結果也包含在 Primitives 就會予以通過呢。
重點 4. 泛用型別化名的型別參數限制 Type Constraint in Generic Type Alias
某泛用型別化名
GT之型別參數Tparam被限制在某型別Tcontraint範圍之下,可以使用extends關鍵字,其格式為:
則該型別
GT一但被註記到某變數時,裡面的型別參數不能代入超出Tconstraint以外的型別值。若
Tconstraint為一系列型別T1、T2、...、Tn的union結果,則Tparam代入的型別可以為T1、T2、...、Tn任意組合union過後的結果。泛用介面與泛用類別的型別參數限制的語法皆相同。
讀者試試看
- 若是出現
intersection的狀況,型別的限制行為為何呢?讀者也可以想想看為何不太需要討論到intersection的型別限制狀況。(提示:某些intersection狀況會導出never型別,亦或者是intersection過後的結果不太需要使用參數型別的限制;所以才說第一篇章《前線維護》很重要吧~)
- 回歸
union的情境,如果以上的範例將User這個泛用型別的參數限制改成union的形式,參數型別限制的狀況會如何?
不過,讀者可能又會問 —— 有沒有單純限制型別參數範疇為 Primitives 中的其中一種型別而非複合過後的型別呢?
恩... 有的 XD,但這要用到條件型別的寫法,這邊筆者暫時展示一下但不會多作探討。(如果筆者認為真的有必要會深究下去,啊~~~好想要探究下去)

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

圖六:自然而然,number | string 是不能被 TypedPrimitiveArray 混用狀態

圖七:不過這個錯誤訊息很奇怪,而且 invalidPrimitiveUnionedArr 被推論的結果是 number[] | string[]
筆者認為目前沒有必要講以上的程式碼的機制,因為超出本日內容範疇;然而,筆者倒是可以劇透一下:
條件型別(Conditional Type)針對
union過後的複合型別具有分配性質(Distributive Property),跟數學上的分配律很像。詳細可以查詢官方對於條件型別的描述內容。
貼心小提示
如果讀者看到
never這個型別還沒領會它的真諦,請參見本系列探討never型別的篇章。很重要喔!如果你只認為
never就只有 Handle Error 的情形,但你不知道never背後代表的深層意義,誠心建議真的要再回頭看看本系列文章!
泛用的函式,筆者特別跟其他泛用型別化名隔離掉的最主要原因也是變化性特多,相信看過上一篇介紹泛型的讀者應該也會知道 —— 泛用的函式型別之函式的參數可以註記為泛用函式所宣告的型別參數。(這句話真繞口令)
不想看繞口令,請看看以下範例:

以上面的程式碼為範例,traverseElements 擁有一個型別參數 —— 該函式的目的是要遍歷一個陣列:
values 對應型別為 Array<T>
(el: T, index: number) => void
假設我們想要遍歷一個數字型別的陣列,於是可以這樣做:

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

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

讀者應該會發現,筆者把除了 traverseElements 的泛用型別參數的註記外,其他的註記都刪掉了。
問題是,筆者在很久很久以前的函式型別篇章有明確講到:
若宣告一個函式的時候,該函式的參數必須要進行註記,不然會無緣無故被 TypeScript 判定為
any型別
不過!筆者也提到:
某些情況下,我們不需要對函式的參數進行積極註記的動作(參見陣列與函式篇章)
其中,最大的原因是因為 —— 泛用型別的參數被指定後,函式內部的參數型別推論也跟著被固定。
讀者請以 TypeScript 編譯器的角度試想:
今天有人告訴你
traverseElements的定義如上面的範例程式碼所示,另外又有人告訴你traverseElements使用時將該型別參數T註記為number型別,是不是也就等同於 ——values這個traverseElements的參數之型別被鎖定為Array<number>以及callback這個參數的型別被鎖定為(el: number, index: number): void。也就是說,就算
callback裡面的參數代入的是:
這種參數沒有被註記過後的函式,只要你看到
traverseElements之型別參數被代入number,你就可以間接推論該回呼函式的參數型別呢?(callback函式之參數型別推論結果如圖八)

圖八:就算 callback 函式裡面的參數沒有被註記,el 這個參數也會被自動推論為 number
這也是通常 Array.prototype 系列的方法,裡面只要有包含需要代入回呼函式的情形,通常也不需要註記回呼函式的參數之型別:
早在你宣告一個陣列時,藉由隱藏在陣列裡的泛用機制,編譯器早就可以推論出回呼函式的參數型別。

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

圖十:只要將 Array<T> 轉換成另類型別,以這個例子來看,Array<number> 型別的值使用 Array.prototype.map 方法的提示性說明變換成 callbackFn: (value: number ...) => unknown,彈性真的很高。
所以筆者得出一個結論:泛用型別可以推論未來的型別使用的可能性,這也就是為何讀者不需要每一次都要進行積極註記的動作,有時候泛用型別的機制就可以讓 TypeScript 推論出正確的結果喔!
重點 5. 泛用函式的宣告 Declaration of Generic Functions
泛用的函式宣告 —— 必須要將型別參數馬上宣告在函式名稱的後面。若某函式所擁有的型別參數有
TP1、TP2、...、TPn,則:
另外,函式的參數以及輸出型別可以為泛用函式所宣告的型別參數或型別參數的各種組合:
重點 6. 泛用型別參數與型別推論的關聯 Relation Between Generic Type Parameter & Type Inference
泛用的型別參數註記最大的用途在於 —— 可以預測未來的程式碼之型別推論的可能性。
重點 6 短短的一句話就是使用泛用型別的最大好處,這也是為何寫 TypeScript 時,你不需要汲汲營營地將每個變數或函式的參數進行積極註記的動作,筆者後面都會來回利用 TypeScript 的泛用型別的這個優勢寫程式碼。
另外,泛用函式之型別參數的宣告也可以使用重點 3 與 4 講過的預設型別(Default Type)與型別限制(Type Constraint),這一點筆者就不再贅述,用法寫法差不多。
這一篇都在打泛用型別的基礎以及一些功能,斤斤計較的地方看起來也很多,不過有了這些基礎,筆者可以開始寫泛用型別的應用囉~