
閱讀本篇文章前,仔細想想看
- 迭代器(Iterator)與聚合物(Collection)的差別在哪?
- 迭代器模式要如何實踐?實踐的目的為何?
- 什麼是多型巡訪(Polymorphic Iteration)?
如果還不清楚可以看一下前一篇文章喔~
今天的主題對於任何本系列的讀者應該是很重要的篇章,相信讀者也是早就碰過 ES2015+ 的語法,但是想要再進修才會學習原生 JavaScript 結合 TypeScript 的型別系統下去。
tsconfig.json 裡的 lib 設定裡新增 es2015這已經在專案編譯篇章裡面已經講過了。
反正就是在 tsconfig.json 裡,將 es2015 選項新增到 lib 設定裡喔。
{
  "compileOptions": {
    "lib": ["dom", "es2015"]
  }
}
Map & Set筆者先從比較單純的東西講,Map 跟 Set 在 TypeScript 的泛型用法。
貼心小提示
這裡筆者就預設讀者已經知道 ES6
Map與Set的用法囉~但是如果沒有用過的話可以看這裡:另外,沒有用過的讀者可能會疑惑,為何要講這個東西 —— 理由以下筆者就會解釋。
ES6 Map 是用來儲存鍵值對的物件,跟普通的 JSON 物件感覺很像,但是差別在於:
重點 1. ES6 Map V.S. JSON Object
ES6
Map可以使用任何型別(不局限於字串)的值作為鍵(key);但普通的 JSON 物件 —— 儘管看似可以使用字串和數字作為鍵,但任何型別的值作為 JSON 物件的鍵(key)都會被轉換成字串型態。
然後,因為 Map 可以使用任何型別的值作為鍵,因此這裡就是泛用之型別參數可以代表的地方;不過,理所當然地,Map 裡的鍵所對應到的值也可以作為另一個型別參數 —— 也就是說,泛用的 Map 本身有兩個型別參數分別代表鍵與值的型別。(以下程式碼是可以正常使用的,讀者可以自行試試看)

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

圖一:使用 Map.prototype.set 時,TypeScript 會彈出視窗提示
另一個筆者認為很好用的資料結構為 ES6 提供的 Set:
重點 2. ES6
Set與普通的列表狀結構(如:Array)的差別
Set為數學上集合的一種體現,因此代表內部所存取的元素符合:
- 無序性:沒有任何順序可言
- 互異性:沒有任何元素重複,每個元素互為不同
- 確定性:元素不外乎只有存在於或者是不存在於集合裡面,這兩種情形
但普通的陣列:
- 可以存在重複的元素
- 內部的元素具有順序性
而 ES6
Set使用起來比起陣列可以更輕易地進行不同Set物件的聯集(union)、差集(difference)或交集(intersection)。
理所當然地,我們可以提供型別參數的值代表 Set<T> 內部存的元素型別。

重點 3. 泛用
Map與Set物件若建構一個 ES6
Map型別的物件,建議提供兩個型別參數的值分別代表Map<Tkey, Tvalue>的鍵與值的對應型別。若建構一個 ES6
Set型別的物件,建議提供一個型別參數的值代表Set<T>內部元素所存取的型別。
讀者試試看
如果是沒有提供型別參數的情況下,以下的
unspecifiedTypeMap1與unspecifiedTypeSet1分別的推論結果為何?
但是如果是以下的情形,
unspecifiedTypeMap2與unspecifiedTypeSet2分別的推論型別為何?
裡面有無初始值會影響推論結果嗎?
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 讀到時,就會馬上啟用的狀態;這也代表不管你後續有沒有串接
then或catch方法,Promise在被建構的那一刻就已經開始在執行內部的程式碼!
通常簡單的 Promise 物件程式碼可能會長這樣,

sendRequest 本身是非同步的 Action(當然,簡單一點的如:setTimeout 等也是非同步的行為),而如果執行過程有結果就會根據不同的 response.status 結果判斷該 Promise 為 resolved 還是 rejected 狀態。
而後 request 存的 Promise 物件分別對 resolved/rejected 狀態有不同的後續處理方式。
事實上,更好一點的理解形式,筆者就直接畫出圖來。(如圖二)

圖二:Promise 可看作是針對非同步事件進行狀態機的表示形式
不過 Promise 的功用還有很多,像是如果是 resolved 的狀態時,在 [Promise Object].then 裡的回呼函式如果回傳的是另一個 Promise 物件,我們可以不停地一直串聯下去。

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

圖三:更完整的 Promise 運作圖,就連你在 catch 錯誤時,依然可以選擇回傳新的 Promise 物件持續這個狀態機的迴圈下去喔!
回過頭來,Promise 在 TypeScript 裡依然跟泛型的使用有關 —— 也就是 Promise<Tresolved> —— 你可以提供一個型別值代表當 Promise 進入 resolved 的狀態時的結果的值之型別。
以下筆者就寫個簡單範例:

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

圖四:顯示數字 200 不為 string | PromiseLike<string> | undefined 型別
其實光是錯誤訊息就透露 —— 連 undefined 也就是空值可以視為 resolve 可填入的東西,至於 PromiseLike<string> 可以想成可以填入類似 Promise.resolve('Succeeded') 這種東西。
不過筆者依然想不透什麼情形會寫成 resolve(Promise.resolve('Succeeded'))。
另外,如果你註記為 Promise<string> 時,使用該 Promise 物件的 Promise.prototype.then 方法則是會提示參數的型別。(如圖五)

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

好,還是很亂 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 差不多。(如圖六)

圖六: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 提問做補充。
讀者試試看
這幾題比起剛剛的
Map與Set還要來得重要,請讀者務必要親手驗證過以下的行為。
- 請問如果我們不提供型別參數的值給
Promise物件,以下的unspecifiedTypePromise的推論型別為何?
- 但如果假設,
Promise物件裡的resolve函式被呼叫時有填入特定型別之值,則unspecifiedTypePromise此時的推論結果為何?
請讀者根據題目 1 與 2 的結果推論:
Promise<T>中,T必須主動註記的必要性 —— 我們是否應當積極註記Promise<T>而非Promise而已?
如果是直接用
Promise.resolve或Promise.reject,請問個別的推論結果為何?
若讀者對於
unknown型別有問題的話,請參見anyv.s.unknown型別篇章。
筆者以下再測試幾個不同常見的 Promise 物件的功能。
Promise.all —— 當所有的 Promise 進入 resolved 狀態時執行Promise.all 的概念有點像是很多不同的 Promise 在同一個時刻開始運行,直到所有在 Promise.all 內部的 Promise 都成功 resolve —— Promise.all([ ... ]).then 才會被執行。
以下的範例程式碼,Promise.all(...) 的推論結果為 Promise<[string, number, boolean]> —— 該型別參數代表的是元組型別。(如圖七)


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

圖八:儘管很明顯筆者刻意要用 Promise.reject 讓 Promise.all 壞掉,但事實上它還是會顯示元組型別格式的推論結果
Promise.race —— 所有的 Promise 進行比賽,誰先 resolve 誰就獲勝這邊很明顯應該不會是用元組型別來顯示結果,而是會用 union 複合型別方式呈現推論結果,因為 Promise.race 裡的每個 Promise 都有被 resolve 的可能。(以下範例程式碼推論結果如圖九)


圖九:推論結果為 Promise<string | number | boolean>
以上的程式碼,筆者只是簡簡單單地建構一個 delay<T> 函式,目的是將 Promise 延緩幾個時間 resolve。
Promise.race 通常好用的地方在於實現 Request Timeout 功能:

比如說,你有一個 arbitraryRequest 是為一個 Promise(或 PromiseLike)物件,但是你希望這個請求能夠在三秒內處理完畢,如果沒有就 reject 掉,你可以使用 Promise.race 並且將該請求跟一個計時器比賽 —— 如果計時器獲勝則代表該 Promise 可以執行 reject 過後的狀態。
筆者在本篇大概講最多的應該是 Promise<T> 這個物件的型別以及推論機制與結果,讀者應該也從這一篇發現泛用型別的重要性了吧~
下一篇筆者要緊接著正進階的部分 —— 泛用型別與 ES2015+ 非同步語法的結合應用喔~