Typescript Pattern Matching
以往在專案中在做一些比較會使用 if else
, switch
等等判斷式,但兩種適用場景不同,if else
在較少並且多個不相關聯的資訊判定時使用,一旦條件多起來以該語法結構來說會變得不易讀,而 switch
則是針對單一資訊判定做出不同行為。
switch
看起來適用場景相當受限,雖然可以用一些方法作到 if else
效果,但不管是哪一種都有個問題是他們都不是表達式,也就是說在一個大型函數中某些值需要使用 if else
, switch
這種判斷會使函數變得混亂,因為你必須得用附值的方式來得到結果給下方的其他操作繼續使用,在沒有良好的分類及區隔上會變得難以閱讀。
function bigFn(){
// ...
let a = null;
if (condition){
a = "a";
} else if (condition){
a = "b";
} else {
a = "c";
}
let b = null;
switch (value){
case "a":
b = "a";
break;
case "b":
b = "b";
break;
case "c":
b = "c";
break;
default:
b = "d";
}
// ...
}
為了解決這種問題,剛好 lodash/ramda 有提供 cond
函數來做到這件事,雖然解決不是表達式的問題,卻衍生另一個型別推斷問題。
自動推導的輸入輸出是依據你每個條件所下函數所決定
在不直接指定輸入輸出型別的情況下會依據你每個條件內的判斷式輸入得到輸入型別,輸出型別是由第二的函數輸出所提供。
// 自動推斷輸入是 number 輸出是 string
cond([
[(x: number) => x === 0, (x: number) => (x + 1).toString()],
])
大部分推斷並不是那麼理想,很多時候會使用 eq
函數,但是 eq
函數的型別長這樣
interface LodashEq {
(self: any, other: any): boolean;
(self: any): (other: any) => boolean;
}
這時你就會發現你的輸入會被自動推斷成 any
// 自動推斷輸入是 any 輸出是 string
cond([
[eq(0), (x: number) => (x + 1).toString()],
])
如果你很聰明的像 ramda 那樣,不如就兩者都必須是相同型別才能用 eq
判斷,但這種型別的覆寫跟原始 eq
函數所提供的功能就沒有完全相同,並且會影響到整個專案 eq
的使用,還有這不是只有 eq
有問題,其他函數也有可能有相同狀況。
勤勞的寫上輸入輸出型別
如果你真的很勤勞的寫上輸入輸出的型別,那非常棒,但是在前端這個到處都是角括號的世界寫上型別似乎讓解讀上更困難了,如果是簡單的 string
, number
等等原始型別可能影響沒那麼大,但如果是物件呢?
cond<{ fieldA: string; fieldB: number }, string>([
[where({ fieldA: eq("a"), fieldB: eq(1) }), (x) => {/* ... */}]
])
因為以上幾種狀況,導致 cond
這個函數處在一個尷尬的狀態,用起來不像 if else
, switch
那麼符合直覺,自動型別推斷上也有諸多缺陷,那我們不如直接找一個功能較為完整的 Pattern Matching 函式庫來取代。
Pattern Matching 函式庫
目前 JS TC39 雖然有關於 Pattern Matching 的提案,不過還在 stage 1,似乎沒什麼推進,等他出來我不知道我還有沒有寫 JS 🙃,所以目前有兩個主要選擇:
我能簡單的說這兩個函式庫所提供的功能幾乎是一模一樣,只差在風格不同,ts-pattern 是走鏈式風格,Effect-TS 是走組合風格,在比對分支上的撰寫模式個人認為幾乎無差異。
那主要的差異就在鏈式風格相對於組合風格不容易做 Tree Shacking,但我認為這也不成問題,ts-pattern 本身就只提供 Pattern Matching 的功能,做不做 Tree Shacking 好像差異並不那麼明顯,Effect-TS 本身會有一些函數會跟自己其他模組功能做一些連動,走組合式風格本來就更適合他們。
令人驚豔的一部分
在我嘗試使用之前我似乎太低估他們了,兩者皆在型別上做到了未涵蓋的情境判定(exhaustive),原本我只是認為頂多未配對到就會報錯而已,但在巢狀結構的錯誤訊息上 ts-pattern 處裡得更好一些,更一目了然的缺少哪些情境,而 Effect-TS 的 exhaustive
目前似乎沒有做到很智慧的多層巢狀判定,必須從輸入的型別層面用 union type 的方式撰寫可能情境,有些麻煩。