Typescript 一些令人又愛又恨的內(nèi)容 — Type Guard、Narrowing
本文已經(jīng)過授權(quán)發(fā)布。
由于 JavaScript 本身是弱語言,因此在開發(fā)上常因?yàn)椴恢雷兞康念愋褪鞘裁炊械娇鄲溃词菇栌擅姆绞阶屪兞康亩ㄎ簧晕⒚鞔_一點(diǎn),我們還是很難一眼就知道他的類型甚至當(dāng)此變量是一個(gè) object 時(shí)我們更難知道里面有哪些 key,因此大家漸漸開始使用 TypeScript 作為主要的開發(fā)工具。
不曉得大家在利用 TypeScript 進(jìn)行開發(fā)時(shí),有沒有覺得 TypeScript 在檢查類型這塊特別惱人,雖然知道這些類型檢查的舉動(dòng)是非常好的,可以幫助我們減少許多可能會(huì)發(fā)生的潛在錯(cuò)誤,今天就要來談?wù)劗?dāng)我們?cè)陂_發(fā)上遇到這種問題時(shí)該如何解決。
場(chǎng)景一
不曉得大家有沒有遇過這種問題,今天想要讓這個(gè)變量查看是否符合 enum 中的某一個(gè)值,結(jié)果 TypeScript 就噴錯(cuò)給你看了,像下面這樣。
其實(shí)要解決上面的紅字方法非常多,首先是開大絕使用 @ts-ignore 讓錯(cuò)誤消失,當(dāng)然這個(gè)方法非常不好,等于是叫 TypeScript 不要檢查下面這行了。
這時(shí)候可能會(huì)想到另一個(gè)方法,上面的錯(cuò)誤信息是說 male 沒有被 assign 到 GENDER 這個(gè) type,所以我只要強(qiáng)制塞給他這個(gè) type 就好,就像這樣:
可是這樣寫仍然不好,等于你強(qiáng)制轉(zhuǎn)變這個(gè)變量了,讓這個(gè)變量失去了彈性,接下來我們介紹比較好用的方法,就讓我們繼續(xù)看下去吧!
Type Guard
首先要介紹的是 Type Guard,Type Guard 顧名思義就是類型的看守者,剛剛 TypeScript 會(huì)報(bào)錯(cuò)就是因?yàn)? type 不一樣,所以只要我們建立一個(gè)類型的看守者,讓 TypeScript 知道這個(gè)變量一定會(huì)符合我 enum 中的某一個(gè) value 時(shí),這時(shí)候就不會(huì)出現(xiàn)紅字了,而通常 Type Guard 會(huì)寫成一個(gè) function 像這樣:
const assertsIsGender = (gender: any) : gender is GENDER => {
return Object.values(GENDER).includes(gender)
}
這時(shí)候我們可以發(fā)現(xiàn) gender 這個(gè)變量已經(jīng)從 string type 變成 GENDER type 了,所以即便我很無聊的再做一次 includes 的判斷 TypeScript 也不會(huì)報(bào)任何錯(cuò)誤了。
這邊我在指定 gender 這個(gè)值之前先指派這個(gè)變量是一個(gè) string type,這個(gè)動(dòng)作很重要,如果沒有先指派變量類型再給值的話這個(gè)變數(shù)就沒辦法順利改變 type 了。
場(chǎng)景二
不曉得大家有沒有遇過在 API 回傳的資料,也會(huì)因?yàn)橘Y料對(duì)應(yīng)到的 enum 的值不同而發(fā)生錯(cuò)誤,像下面這樣:
有了上面 Type Guard 的觀念后,這時(shí)候的讀者一定知道要寫一個(gè) function 來處理這段錯(cuò)誤信息:
的確錯(cuò)誤信息沒有了,但很奇怪的是 gender 竟然變成 never type 了,而這個(gè)就是 Type Guard 會(huì)做到的一個(gè)類型保護(hù)機(jī)制叫:Narrowing。
類型收窄(Narrowing)
Narrowing 翻成白話文就是類型收窄,在 TypeScript 的世界中每一個(gè) enum 基本上都是獨(dú)立存在彼此之間是沒有交集的,關(guān)系圖就像下面這樣:
所以要進(jìn)行兩個(gè) enum 間的類型轉(zhuǎn)換就很容易產(chǎn)生出一個(gè)可能不會(huì)存在的型別,對(duì)于可能不會(huì)存在的類別 TypeScript 把這個(gè)型別定義為 never,而這時(shí)候當(dāng)我們使用了 Type Guard 的技巧,TypeScript 就會(huì)自動(dòng)把類型收窄成 never type,而不是自動(dòng)轉(zhuǎn)換成另一個(gè) enum 了。
當(dāng)然聰明的你可能會(huì)這樣想:那我只要把 function return 定義成另一個(gè) enum 不就好了,這樣就可以確保我 Type Guard 的結(jié)果一定會(huì)類型轉(zhuǎn)換成我想要的 enum,像下面這樣:
這樣寫看起來的確沒有什麼問題,我們想要的結(jié)果也從類型收窄變成了類型轉(zhuǎn)換,但這樣做其實(shí)就有點(diǎn)不太符合 Type Guard 的精神,畢竟 Type Guard 要做的是類型檢查而不是類型轉(zhuǎn)換,而且假如我們要做的是類型轉(zhuǎn)換,這樣寫也會(huì)讓這個(gè) function 的復(fù)用性不高,因此我們接下來要介紹比較好的類型轉(zhuǎn)型方法。
Mapper enum
首先我們可以先想想如何讓類型轉(zhuǎn)換這件事被復(fù)用,我們不妨把想法簡(jiǎn)單化,就是建立一個(gè) function 把 A 型態(tài)轉(zhuǎn)換成 B 型態(tài),而這時(shí)候就必須要利用 TypeScript 中的 Generics 泛型這個(gè)技巧了,像下面這樣:
const createEnumMapper = <T>(mapping: T) => (value: keyof T | null) : T[keyof T] | undefined => {
return value === null ? undefined : mapping[value]
}
這個(gè) createEnumMapper 的 function 是一個(gè) currying function,第一個(gè)變數(shù)傳入的是 enum 本身,這時(shí)候 TypeScript 的 Generics 就會(huì)知道我的 T 就是跟 enum 本身有關(guān)。
為了讓這個(gè) Generics 可以正確的把兩個(gè) enum mapping起來,我們必須要先建立一個(gè) object 把兩個(gè) enum 的key value配對(duì)像下面這樣:
const mapper = {
[BE_GENDER.MALE]: FE_GENDER.MALE,
[BE_GENDER.FEMALE]: FE_GENDER.FEMALE
}
由于我們上面的 mapper 是把 enum 的 value 當(dāng)成 key,所以我們只要帶入 data 的值就可以直接轉(zhuǎn)換了,像下面這樣:
這時(shí)候就可以發(fā)現(xiàn)我們成功的把 BE_GENDER type 的值轉(zhuǎn)成 FE_GENDER type 的值了,而且也不需要?jiǎng)佑玫?Type Guard 的觀念。
總結(jié)
今天介紹了 TypeScript 中用來檢查類型的方法,假如讀者日后遇到類似這種問題不妨可以多加利用 Type Guard 進(jìn)行檢查,而不是直接開大絕用 @ts-ignore 或者 as 這兩種方法,除了介紹類型檢查外也介紹了如何進(jìn)行類型轉(zhuǎn)換,希望這些方法都可以讓讀者未來在使用上都不會(huì)有太多的問題。