iT邦幫忙

第 11 屆 iThome 鐵人賽

1
Modern Web

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

Day 47. 通用武裝・泛型應用 X 結合 ES2015+ - TypeScript Generics with ES2015+ Features

https://ithelp.ithome.com.tw/upload/images/20191014/20120614FSVLJJ3eDH.png

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

  1. 迭代器(Iterator)與聚合物(Collection)的差別在哪?
  2. 迭代器模式要如何實踐?實踐的目的為何?
  3. 什麼是多型巡訪(Polymorphic Iteration)?

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

今天的主題對於任何本系列的讀者應該是很重要的篇章,相信讀者也是早就碰過 ES2015+ 的語法,但是想要再進修才會學習原生 JavaScript 結合 TypeScript 的型別系統下去。

ES2015+ 與泛用的結合

請記得一定要在 tsconfig.json 裡的 lib 設定裡新增 es2015

這已經在專案編譯篇章裡面已經講過了。

反正就是在 tsconfig.json 裡,將 es2015 選項新增到 lib 設定裡喔。

{
  "compileOptions": {
    "lib": ["dom", "es2015"]
  }
}

ES6 鍵-值對物件與集合物件 —— Map & Set

筆者先從比較單純的東西講,MapSet 在 TypeScript 的泛型用法。

貼心小提示

這裡筆者就預設讀者已經知道 ES6 MapSet 的用法囉~但是如果沒有用過的話可以看這裡:

  1. ES6 Map MDN Documentation
  2. ES6 Set MDN Documentation

另外,沒有用過的讀者可能會疑惑,為何要講這個東西 —— 理由以下筆者就會解釋。

ES6 Map 是用來儲存鍵值對的物件,跟普通的 JSON 物件感覺很像,但是差別在於:

重點 1. ES6 Map V.S. JSON Object

ES6 Map 可以使用任何型別(不局限於字串)的值作為鍵(key);但普通的 JSON 物件 —— 儘管看似可以使用字串和數字作為鍵,但任何型別的值作為 JSON 物件的鍵(key)都會被轉換成字串型態。

然後,因為 Map 可以使用任何型別的值作為鍵,因此這裡就是泛用之型別參數可以代表的地方;不過,理所當然地,Map 裡的鍵所對應到的值也可以作為另一個型別參數 —— 也就是說,泛用的 Map 本身有兩個型別參數分別代表鍵與值的型別。(以下程式碼是可以正常使用的,讀者可以自行試試看)

https://ithelp.ithome.com.tw/upload/images/20191014/20120614u4NCxFj81k.png

另外,筆者想要強調,善用型別系統的推論部分 —— 比如,如果臨時忘記 Map.prototype.set 方法要填的參數與對應型別,TypeScript 會自動跟你提示。(如圖一)

https://ithelp.ithome.com.tw/upload/images/20191014/20120614gjjsIR2XWV.png
圖一:使用 Map.prototype.set 時,TypeScript 會彈出視窗提示

另一個筆者認為很好用的資料結構為 ES6 提供的 Set

重點 2. ES6 Set 與普通的列表狀結構(如:Array)的差別

Set 為數學上集合的一種體現,因此代表內部所存取的元素符合:

  • 無序性:沒有任何順序可言
  • 互異性:沒有任何元素重複,每個元素互為不同
  • 確定性:元素不外乎只有存在於或者是不存在於集合裡面,這兩種情形

但普通的陣列:

  • 可以存在重複的元素
  • 內部的元素具有順序性

而 ES6 Set 使用起來比起陣列可以更輕易地進行不同 Set 物件的聯集(union)、差集(difference)或交集(intersection)。

理所當然地,我們可以提供型別參數的值代表 Set<T> 內部存的元素型別。

https://ithelp.ithome.com.tw/upload/images/20191014/20120614uP6Cz4S6rT.png

重點 3. 泛用 MapSet 物件

若建構一個 ES6 Map 型別的物件,建議提供兩個型別參數的值分別代表 Map<Tkey, Tvalue> 的鍵與值的對應型別。

若建構一個 ES6 Set 型別的物件,建議提供一個型別參數的值代表 Set<T> 內部元素所存取的型別。

讀者試試看

如果是沒有提供型別參數的情況下,以下的 unspecifiedTypeMap1unspecifiedTypeSet1 分別的推論結果為何?

https://ithelp.ithome.com.tw/upload/images/20191014/201206145fGsiewDkc.png

但是如果是以下的情形,unspecifiedTypeMap2unspecifiedTypeSet2 分別的推論型別為何?

https://ithelp.ithome.com.tw/upload/images/20191014/20120614oiBzt3ZbKr.png

裡面有無初始值會影響推論結果嗎?

ES6 Promise 物件

事實上,之前在模擬戰 —— UBike 篇章有稍微使用過 Promise 物件了,不過暫且還是簡介一下:

重點 4. ES6 Promise 物件的用意與目的

Promise 物件可以針對非同步的事件(Asynchronous Events)進行狀態機(State Machine)式的編程操作,狀態有:

  • pending當 Promise 物件被 JS Engine 讀到時,就會馬上啟用的狀態pending 意指等待內部的程式碼的結果
  • resolved:Promise 內部的非同步過程執行成功時的結果
  • rejected:Promise 內部的非同步過程執行失敗時的結果

對於 Promise 有任何問題,社群上有很多熱心的人們已經把 Promise 講到不能筆者覺得不能再講下去了 XD,而且 MDN Document 也寫得清清楚楚。

貼心小提示

筆者還是再三強調一次:當 Promise 物件被 JS Engine 讀到時,就會馬上啟用的狀態;這也代表不管你後續有沒有串接 thencatch 方法,Promise 在被建構的那一刻就已經開始在執行內部的程式碼

通常簡單的 Promise 物件程式碼可能會長這樣,

https://ithelp.ithome.com.tw/upload/images/20191014/20120614cm0MfBSATL.png

sendRequest 本身是非同步的 Action(當然,簡單一點的如:setTimeout 等也是非同步的行為),而如果執行過程有結果就會根據不同的 response.status 結果判斷該 Promiseresolved 還是 rejected 狀態。

而後 request 存的 Promise 物件分別對 resolved/rejected 狀態有不同的後續處理方式。

事實上,更好一點的理解形式,筆者就直接畫出圖來。(如圖二)

https://ithelp.ithome.com.tw/upload/images/20191014/201206146YgsclLifq.png
圖二:Promise 可看作是針對非同步事件進行狀態機的表示形式

不過 Promise 的功用還有很多,像是如果是 resolved 的狀態時,在 [Promise Object].then 裡的回呼函式如果回傳的是另一個 Promise 物件,我們可以不停地一直串聯下去。

https://ithelp.ithome.com.tw/upload/images/20191014/20120614rEv936UT3u.png

這樣的行為用更完整的圖會是這樣的呈現。(如圖三)

https://ithelp.ithome.com.tw/upload/images/20191014/201206142bTePWplM3.png
圖三:更完整的 Promise 運作圖,就連你在 catch 錯誤時,依然可以選擇回傳新的 Promise 物件持續這個狀態機的迴圈下去喔!

回過頭來,Promise 在 TypeScript 裡依然跟泛型的使用有關 —— 也就是 Promise<Tresolved> —— 你可以提供一個型別值代表當 Promise 進入 resolved 的狀態時的結果的值之型別。

以下筆者就寫個簡單範例:

https://ithelp.ithome.com.tw/upload/images/20191014/20120614IzH5NyQOFu.png

當你特別註記 Promise<string> 就代表:resolve() 內部的值必須填入 string 型別,如果不是的話就會顯示錯誤訊息如圖四。

https://ithelp.ithome.com.tw/upload/images/20191014/20120614nPyINSyRZ8.png
圖四:顯示數字 200 不為 string | PromiseLike<string> | undefined 型別

其實光是錯誤訊息就透露 —— 連 undefined 也就是空值可以視為 resolve 可填入的東西,至於 PromiseLike<string> 可以想成可以填入類似 Promise.resolve('Succeeded') 這種東西。

不過筆者依然想不透什麼情形會寫成 resolve(Promise.resolve('Succeeded'))

另外,如果你註記為 Promise<string> 時,使用該 Promise 物件的 Promise.prototype.then 方法則是會提示參數的型別。(如圖五)

https://ithelp.ithome.com.tw/upload/images/20191014/20120614eEfXdrpg4S.png
圖五:then 裡面的提示

乍看之下裡面內容挺恐怖的,但是整理過後仔細瞧瞧:

https://ithelp.ithome.com.tw/upload/images/20191014/20120614ZR79S4ri9a.png

好,還是很亂 XDD,但慢慢來解析。

onfulfilled?選用屬性,可以填入一個函式型別 —— 該函式的參數為 value: string,跟原本的 Promise<string> 裡提供的 string 型別參數的值連結。而輸出部分則除了 string 與 Nullable Types 以外,還可以填入 PromiseLike<string>,也就是指出剛剛筆者在圖三時有提到的:Promise 物件可以串下去的行為。

另外的 onrejected? 則是當 Promise 物件裡的非同步程式碼執行有出錯或者是被使用者主動呼叫 reject 的話觸發的行為。裡面也可以填入函式型別,其參數型別為 reason: any,這一點之所以沒有跟 Promise 物件的型別參數進行連結的原因,可想而知,錯誤的出現形式可不會只是字串而已,光是物件的組合也挺多種,因此才是少數會用到 any 型別的情形。

至於 PromiseLike<never> 就是指這個 Promise 本身無法完整地執行完畢,直接拋出錯誤的概念。(參見 never 型別篇章

同理,Promise.prototype.catch 方法裡的敘述跟 onrejected?Promise.prototype.then 差不多。(如圖六)

https://ithelp.ithome.com.tw/upload/images/20191014/20120614FtPVvpZVLe.png
圖六:Promise.prototype.catch 的提示內容

重點 5. 泛用 ES6 Promise 物件

在 TypeScript 的世界裡,Promise<Tresolved>Promise 物件的完整泛用表示式。而型別參數 Tresolved 代表 Promise 物件時,呼叫 resolve 方法可以填入的型別外,還代表未來在使用 Promise.prototype.then 方法時,回呼函式的參數代表之型別。

此外,resolve 時除了可為 Tresolved 型別外,也可以是空值(undefined)以及類似於 PromiseLike<string> 的型別之值。

貼心小提示

讀者應該還是會感到疑惑,為何有所謂的 xxLike 型別的東西 —— 比如 ArrayLike<T>PromiseLike<T> 等。

提示:跟 TypeScript lib 設定有關。

由於這個是進階性的話題,筆者還是給讀者一帖 StackOverflow 提問做補充。

讀者試試看

這幾題比起剛剛的 MapSet 還要來得重要,請讀者務必要親手驗證過以下的行為

  1. 請問如果我們不提供型別參數的值給 Promise 物件,以下的 unspecifiedTypePromise 的推論型別為何?

https://ithelp.ithome.com.tw/upload/images/20191014/20120614o01ZG5Vnnf.png

  1. 但如果假設,Promise 物件裡的 resolve 函式被呼叫時有填入特定型別之值,則 unspecifiedTypePromise 此時的推論結果為何?

https://ithelp.ithome.com.tw/upload/images/20191014/20120614N33vVpfz9R.png

  1. 請讀者根據題目 1 與 2 的結果推論:Promise<T> 中,T 必須主動註記的必要性 —— 我們是否應當積極註記 Promise<T> 而非 Promise 而已?

  2. 如果是直接用 Promise.resolvePromise.reject,請問個別的推論結果為何?

https://ithelp.ithome.com.tw/upload/images/20191014/201206144PxwZAm0Nb.png

若讀者對於 unknown 型別有問題的話,請參見 any v.s. unknown 型別篇章

筆者以下再測試幾個不同常見的 Promise 物件的功能。

Promise.all —— 當所有的 Promise 進入 resolved 狀態時執行

Promise.all 的概念有點像是很多不同的 Promise同一個時刻開始運行,直到所有在 Promise.all 內部的 Promise 都成功 resolve —— Promise.all([ ... ]).then 才會被執行。

以下的範例程式碼,Promise.all(...) 的推論結果為 Promise<[string, number, boolean]> —— 該型別參數代表的是元組型別。(如圖七)

https://ithelp.ithome.com.tw/upload/images/20191014/20120614RFu7MW7vCv.png

https://ithelp.ithome.com.tw/upload/images/20191014/20120614lxgyqSc021.png
圖七:以上的程式碼,Promise.all 此時的推論結果

如果刻意將其中一個故意 reject 掉,儘管看似應該要回傳類似 Promise<never> 這種會出現錯誤的狀態,但它仍然會按照元組格式去顯示結果。(如圖八,不過這種行為依筆者來看應該是錯的)

https://ithelp.ithome.com.tw/upload/images/20191014/20120614Cz3nX5Xztj.png
圖八:儘管很明顯筆者刻意要用 Promise.rejectPromise.all 壞掉,但事實上它還是會顯示元組型別格式的推論結果

Promise.race —— 所有的 Promise 進行比賽,誰先 resolve 誰就獲勝

這邊很明顯應該不會是用元組型別來顯示結果,而是會用 union 複合型別方式呈現推論結果,因為 Promise.race 裡的每個 Promise 都有被 resolve 的可能。(以下範例程式碼推論結果如圖九)

https://ithelp.ithome.com.tw/upload/images/20191014/20120614IiNWRwaaC1.png

https://ithelp.ithome.com.tw/upload/images/20191014/20120614BxVQkiwAVb.png
圖九:推論結果為 Promise<string | number | boolean>

以上的程式碼,筆者只是簡簡單單地建構一個 delay<T> 函式,目的是將 Promise 延緩幾個時間 resolve

Promise.race 通常好用的地方在於實現 Request Timeout 功能:

https://ithelp.ithome.com.tw/upload/images/20191014/20120614gMCLFfKLp4.png

比如說,你有一個 arbitraryRequest 是為一個 Promise(或 PromiseLike)物件,但是你希望這個請求能夠在三秒內處理完畢,如果沒有就 reject 掉,你可以使用 Promise.race 並且將該請求跟一個計時器比賽 —— 如果計時器獲勝則代表該 Promise 可以執行 reject 過後的狀態。

小結

筆者在本篇大概講最多的應該是 Promise<T> 這個物件的型別以及推論機制與結果,讀者應該也從這一篇發現泛用型別的重要性了吧~

下一篇筆者要緊接著正進階的部分 —— 泛用型別與 ES2015+ 非同步語法的結合應用喔~


上一篇
Day 46. 通用武裝・迭代器模式 X 泛用迭代器 - Iterator Pattern Using TypeScript
下一篇
Day 48. 通用武裝・非同步概念 X 脫離巢狀地獄 - TypeScript Generics with Asynchronous Programming I. Promise Chain
系列文
讓 TypeScript 成為你全端開發的 ACE!51
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言