iT邦幫忙

第 11 屆 iThome 鐵人賽

3
Modern Web

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

Day 42. 通用武裝・泛用型別 X 型別參數化 - TypeScript Generics Introduction

https://ithelp.ithome.com.tw/upload/images/20191008/20120614NLLbGoV0PD.png

《通用武裝》篇章概要

本系列即將邁入後半段(現在才邁入後半段會不會有點晚?)—— 泛用型別(Generics)的介紹。

筆者翻閱很多資料發現,泛用型別儘管看似困難(這是要讓讀者不想學嗎?),但用途事實上真的很多,甚至會延伸到 ES6 的一些 Feature。

筆者在第三篇章的 UBike 地圖案例就有提到 —— ES6 Promise 與 Map 事實上都有用到泛用型別的註記(Type Annotation)表示行為。

以下大概是筆者設想可能會講到的東西:

  • 泛用型別(Generic Types)的基礎行為
  • 各種泛用型別在普通型別、介面與類別的推論與註記機制
  • 預設泛用型別參數 Default Type Parameter
  • 泛用型別限制 Generic Type Constraint
  • 函式型別的參數不需要註記的特殊情形(補第一篇章《前線維護》的坑)
  • ECMAScript 提供的功能層面(Utility Aspect)與 TypeScript 泛型的結合應用
    • ES6 Promise
    • ES6 Map/Set
    • ES6 Generators & Iterators
    • ES7 Async-Await
  • 迭代器模式 Iterator Pattern
  • 非同步編程,如:Callback Hell、Promise Chain、Async-Await
  • 更多介面與類別的結合實作 + 泛型應用
  • 條件型別 Conditional Type(這東西要看筆者情形可能不會講到,但又很想講,ㄊㄋㄋㄉ

同理,筆者在此發出一個聲明:第四篇章的內容順序不一定會按照上面的順序講解,筆者會按照適合的方式進行編排喔~~~

另外,如果是臨時看到這裡但剛學習 TypeScript 的讀者,除非你有 Java、C# 等靜態語言的學習背景,否則還是建議至少把第一篇章《前線維護》部分讀完,因為型別系統的推論與註記機制在本篇章討論的佔比又會回到很大又很變態的篇幅大小。

[2019.10.22 16:49 新增訊息]

由於最近處理的事情變多,連載速度會開始變慢,所以第四篇章還是未完成狀態 XD,但確定會超過 Day 50

另外,本篇章由於圖的品質跟第三篇章有得拼,數量更是多了些,因此速度變慢也是正常的 QQ,請讀者給筆者時間整理一下。

貼心小提示

本系列內容預設編譯器設定 tsconfig.json 裡的 noImplictAnystrictNullChecks 為啟動的狀態。

如果對於這兩個選項有問題可以參見這一篇

以下本篇章第一篇文 — 正文開始

泛用型別的介紹 TypeScript Generics Introduction

泛用的概念 Concept of Generics

開篇就從最簡單看起來也很笨的的例子來舉。

https://ithelp.ithome.com.tw/upload/images/20191008/20120614xHY7zxNPxY.png

Identity 這個型別化名的宣告裡,有一個被稱為型別參數(Type Parameter)的東西 —— 也就是 Identity<T> 裡面的 T 這個東西。

原理跟普通的函式概念差不多,只是可以把泛用型別看成是型別版的函式,代入的 Identity<T> 裡面的 T 為什麼樣的型別 —— T 就會成為那個型別

比如說,Identity<number> 裡面的 T 被取代為 number 型別,將其註記到任何變數會得到如圖一的推論結果:

https://ithelp.ithome.com.tw/upload/images/20191008/20120614bwToFVYCDY.png
圖一:Identity<number> 中,把 T 取代為 number,則 Identity<number> 的結果就等於 number

基本上,泛用型別就是將型別化名進行參數化的動作,帶入不同的型別作為泛用型別的參數,就會得出不同的型別樣貌。

重點 1. 泛用型別的概念 Concept of Generic Types

泛用型別的意義最主要是將型別化名進行參數化(Parameterize)的動作,使得型別化名擁有更多的彈性與變化性。

泛用化名 Generic Type Alias

筆者提到一個很重要的點:泛用型別就是將型別化名進行參數化的動作;也就是說,任何型別化名都可以轉換成泛用型別。

從筆者這樣的描述可以推論:型別(Types)介面(Interface)類別(Class)都可以轉換成泛用型式。

以下筆者就舉幾個例子:

https://ithelp.ithome.com.tw/upload/images/20191008/201206148VQMRExMsn.png

Dictionary<T> 為一個泛用型別,代表的就是一般的鍵值對(Key-Value Pair)的物件格式。其中,如果 T 被代入為 boolean 型別,則如果值裡面含有非 boolean 型別的值就會被 TypeScript 警告。以下就舉幾個例子,展示給讀者看。(程式碼如下,檢測結果如圖二)

https://ithelp.ithome.com.tw/upload/images/20191008/20120614m178KHsSyG.png

https://ithelp.ithome.com.tw/upload/images/20191008/20120614V6HHKdLWhI.png
圖二:因為去旅遊韓國愉快的原因是 string 型別 —— 不符合 Dictionary<boolean> 指定的 boolean 型別,就會被 TypeScript 和諧掉(甘去韓國愉快旅遊 P 事

至於泛用介面與泛用類別的使用情形~ 筆者把他們保留到後續篇章,因為這個討論串一開,筆者一定會不小心啪拉啪拉一連串把本篇擴張到沒完沒了到很難收尾的狀態(深感尷尬),筆者在本篇先把泛型的簡介內容都些丟出來給讀者做心理準備,後面講到一些泛型的應用對讀者來說也比較容易適應。

重點 2. 泛用型別化名 Generic Type Alias

泛用型別的表現形式不單單只有型別(Type)而已,介面與類別皆可以轉換成泛用型式

在泛用型別化名的名稱後面接上的 <> 內容就是型別參數(Type Parameter)的宣告。

型別參數可以有複數個,只要在 <> 裡用逗號分隔就可以了。

詳細的宣告過程細節、規則與程式碼公式化結果(筆者一貫的手法)會在後面的篇章慎重地呈現出來。

泛用函式與泛用參數 Generic Functions & Generic Function Parameters

泛用的形式基本上是很全面地(Universal),連函式本身也可以變成泛用的形式,但是變化性可能對剛接觸泛型的讀者深感困惑。以下筆者把常見的函式型別的泛用型式刻意分成兩種。

函式型別的泛用形式 Function Type Generic Representation

https://ithelp.ithome.com.tw/upload/images/20191008/20120614cuN9SlH1Bg.png

首先,以上的案例宣告一個函式的型別化名 operator —— 這個 operator 有一個名為 T 的型別參數,而剛好 operator 裡面對應的函式型別的參數以及輸出也是 T 型別,就以 operator<number> 為範例的話,p1p2 就會被限制為 number 型別,而輸出的型別也會是 number 型別。

所以以下的 addition 這個變數註記為 operator<number> 就代表 —— p1p2 以及函式輸出必須為 number 型別;而 stringConcatenation 則是 operator<string> 就代表 —— p1p2 以及函式的輸出必須為 string 型別。

https://ithelp.ithome.com.tw/upload/images/20191008/201206145AhV31PjwJ.png

事實上,以上的程式碼還有地方可以簡化,這部分就放在後續篇章討論不然會很難收尾 XD

函式本身就是泛用型式 Function Itself is Generic

https://ithelp.ithome.com.tw/upload/images/20191008/20120614sppIZsgZP7.png

這裡並非是用泛用型別化名註記在變數上,然後再指派一個函式進去;以上的程式碼呈現的方式是 —— 你可以在宣告函式的時候就把它變成泛用的形式

這個 identityFunc 有宣告一個型別參數 T,函式本身只有一個參數,對應型別是 T,而輸出的結果被註記為型別參數 T,也就是說:如果你呼叫該函式時是 identityFunc<string> 這樣呼叫的,輸入必須填數 string 型別,輸出也是 string 型別。

重點 3. 泛用函式表現行為 Generic Function Representation

若宣告的函式型別為泛用形式,泛用的型別參數除了可以註記到函式內部的變數外,還可以註記在函式的參數(Parameter)與輸出(Output)上。

多重泛用參數 Multiple Generic Type Parameters

理所當然,我們還可以使用多重的泛用參數,這其實在本篇的重點 2 的最後一句話有稍微提及,不過筆者暫且就展示給讀者看一下:

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

以上的 TypeConversion 型別有兩個型別參數,分別是 TU

isPositive 為例,它被註記到的 TypeConversion 它的 Tnumber 型別,U 則是 boolean 型別;而 TypeConversion 的輸入為 T 型別,輸出為 U 型別,isPositive 將輸入為 number 型別的東西,根據數學的正數判斷,轉換成 boolean 型別的值。

另外的 anythingToString 則是將 T 設定為 any 型別,U 則是 string,代表輸入可以為任何型別值,但輸出必須是 string,裡面的實作內容也僅僅是對輸入呼叫 toString 這個方法進行轉換。

內建的泛用型別 Built-in Generics

事實上,泛用型別在 TypeScript 裡本來就有存在的痕跡,只是有沒有發覺到而已。

最簡單的就是陣列型別 —— Array<T> 這種型別 —— 它是內建的,等效於 T[]

也就是說,用以下的方式表示也 OK:

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

以上的 MyArray<T> 是為了不要重複定義 Array<T> 而選擇用其他化名取代。實際上除了基本的 T[] 表示形式,也可以直接用 Array<T> 代表某陣列。

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

讀者可以試著檢測看看以上的程式碼,筆者就不貼出結果了。

其中,又以 ECMAScript 新增、偏向功能層面(Utility Aspect)的應用跟泛用型別的機制又息息相關,這又讓筆者不得不搬出 ES6 Promise、Map 與 Set 等東西與泛用型別的結合等應用,這些都會在後續篇章進行探討 —— 畢竟筆者寫作當下不希望讀者會了泛用型別,但是卻不曉得應用或者是哪些地方會看到存在 —— 事實上泛用型別處處存在,尤其對型別的推論(Type Inference)有非常大的影響

沒有泛用型別的話,型別系統少了一半的推論能力也不足以為奇

感覺就是 —— 沒有泛用型別,這型別系統也就爛爛的 —— 所以本篇章也會剖析型別推論除了認讀者的型別註記(Type Annotation)以及根據語法的結構判斷型別推論(Syntatic Structure)外,泛用型別對於型別推論隱藏的巨大影響。

理解這裡面的機制事實上對於用 TypeScript 寫程式,除了會感到特別有趣外,還可以提升寫程式的效率。(如洪荒之力非常多!)

條件型別 Conditional Types

事實上,這也是從泛用型別延伸出來的變體,條件型別(Conditional Types)筆者很少看到有中文的文章在探討(不然就是筆者笨到連 Gxxgle 搜尋都不會用),英文討論條件型別的文章,恩... 不算少,但是也很少人正視這個東西,以為又是 TypeScript 額外延伸出來的功能。

但這並不完全是 TypeScript 的新功能,只能說算是部分新、但又是從泛用型別延伸出來的,因此筆者沒有把條件型別列入型別的一種表現形式的原因主要是因為 —— 它是衍伸物,並不太需要刻意立下一個新的型別種類。

以上廢話太多,不過筆者暫且舉一個還蠻不錯的條件型別案例,而且這本來就是 TypeScript 內建的,不過這些都被官方稱為 Utility Types,冠上的是這個 Utility 這個名稱,實質上是用條件型別的方式去實踐,但本質還是泛用型別的一種延伸出的表現行為。

官方真是麻煩,就好好講說是內建的條件型別也可以,偏偏再多冠上一個 Utility Types 名稱。

“好啦,趕快舉例!筆者不要發牢騷,廢話真多!”

Required 就是一種還蠻好用的條件型別。

很久的之前探討過的選用屬性部分,只要被屬性旁標註為 ? 就代表不一定需要該屬性,可視為不存在。

另外,介面的宣告與註記行為,只要變數被註記到某介面就必須實作全部的功能,並且根據物件完整性的理論,該變數:

  • 不能多或少一個功能
  • 功能對應之型別不能夠不符合介面定義的行為
  • 可以被完全覆寫,但覆寫的格式與每個功能對應的型別都不能出錯

這應該對讀者來說已經是朗朗上口的原則(如果你完整看完本系列的話)。

但是 Required<T> 非常有趣,它的意思是 —— 它會將所有 T 裡面的選用屬性轉成必要的屬性。(圖三為檢測結果;圖四為錯誤訊息)

https://ithelp.ithome.com.tw/upload/images/20191009/20120614625WgIwaoZ.png

https://ithelp.ithome.com.tw/upload/images/20191009/20120614bLca0gYfrc.png
圖三:如果被冠上 Required 這個條件型別,內部的 PersonalInfo 介面會把所有的選用屬性轉成必要屬性

https://ithelp.ithome.com.tw/upload/images/20191009/20120614JU85NNQnnr.png
圖四:TypeScript 很明確地告訴你,Required<PersonalInfo> 的註記下,少了 hasPet 這個屬性

事實上,筆者這裡沒有講條件型別的寫法 —— 因為比泛用型別更複雜......很多 XDDDD。

所以筆者認為這個主題應該會放到本篇章很後面才會講到,但坦白說,讀者真的需要再讀也可以,除非你對於這東西感到興趣。

小結

本篇開篇大致上介紹完讀者會看到的泛型大概會在哪裡出現~

泛型應用挺多的 —— 沒學過或聽過的讀者最好要有心理準備。XDDDDDDDD


上一篇
Day 41. 戰線擴張・模擬戰 — UBike 地圖 X 外觀模式 - Façade Pattern in TypeScript
下一篇
Day 43. 通用武裝・泛型註記 X 推論未來 - TypeScript Generic Declaration & Annotation
系列文
讓 TypeScript 成為你全端開發的 ACE!51
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

1 則留言

0
平民百姓
iT邦新手 4 級 ‧ 2022-01-03 11:58:07

感謝大大的解說,說明得很詳細,很實用

我要留言

立即登入留言