淺談 TS 標(biāo)稱類型介紹及社區(qū)實(shí)現(xiàn)
前言
有位大神說(shuō)過(guò)"程序是類型的證明",我看不懂,但我大受震撼。為了以后能看懂哪怕一點(diǎn)點(diǎn),我決定記錄下類型相關(guān)的所學(xué)所悟。
《淺談 TS 標(biāo)稱類型》系列將以稍偏門的視角來(lái)看待 TypeScript 的類型系統(tǒng),實(shí)際用途不大,但自覺(jué)有趣。本文是該系列的開篇文章,主要介紹標(biāo)簽類型是什么,以及 TS 社區(qū)都有哪些實(shí)現(xiàn)手段。
什么是標(biāo)稱類型系統(tǒng)(nominal type system)
先通俗地理解下,舉個(gè)例子,userId = 123、bookId = 34都是數(shù)字,但兩者用于不同的場(chǎng)景,希望用不同類型 UserID 和 BookID 來(lái)表示,且不能互換。像這樣,數(shù)據(jù)的值本身沒(méi)什么區(qū)別,安上不同名字就是不同類型,這就是標(biāo)稱類型系統(tǒng)(nominal type system)。也就是說(shuō),標(biāo)稱類型系統(tǒng)中,兩個(gè)變量是否類型兼容(可以交換賦值)取決于這兩個(gè)變量顯式聲明的類型名字是否相同。
與之相對(duì)的是結(jié)構(gòu)類型系統(tǒng)(structural type system),類型兼容只取決于實(shí)際結(jié)構(gòu)是否相同,與類型名字無(wú)關(guān)。比如:定義Point類型包含x、y兩個(gè)數(shù)字,rect = { x: 33, y: 3, width: 30, height: 80 }的結(jié)構(gòu)滿足Point的定義,就屬于Point類型。簡(jiǎn)單理解,結(jié)構(gòu)類型系統(tǒng)中,結(jié)構(gòu)或者說(shuō)形狀相同的兩個(gè)值,它們的類型是兼容的,可以交換賦值。
更嚴(yán)格的定義可以看下Type system - Wikipedia的說(shuō)明。
除了上面的 UserID 和 BookID 的例子,標(biāo)稱類型還有其他常見(jiàn)的應(yīng)用場(chǎng)景,比如:區(qū)分不同的字符串(正則表達(dá)式、html模版、文件路徑等),表達(dá)不同單位的量綱(不同幣種的金額、css各種長(zhǎng)度單位)等。這些會(huì)在后續(xù)文章再展開說(shuō)明,屆時(shí)也會(huì)列舉下標(biāo)稱類型常見(jiàn)的錯(cuò)誤用法。
TS 是標(biāo)稱類型系統(tǒng)嗎
不是。TS 是結(jié)構(gòu)類型系統(tǒng)(structural type system),基于結(jié)構(gòu)/形狀檢查類型,而非類型的名字。
One of TypeScript’s core principles is that type checking focuses on the shape that values have. This is sometimes called “duck typing” or “structural typing”.
TypeScript: Documentation - TypeScript for JavaScript Programmers
上面是TS官方文檔的說(shuō)明,里面還舉了一些例子,可以先看看加深理解。
TS 可以實(shí)現(xiàn)標(biāo)稱類型嗎
可以(不然這篇文章寫到這里就要結(jié)束了??)。TS 目前不支持顯式聲明標(biāo)稱類型,也沒(méi)有計(jì)劃支持,2014年的提案Support some non-structural (nominal) type matching · Issue #202 到現(xiàn)在還是Open狀態(tài)。不過(guò)社區(qū)有不少方案,可以基于現(xiàn)有 TS 的能力一定程度上實(shí)現(xiàn)標(biāo)稱類型,整理如下。
TS 實(shí)現(xiàn)標(biāo)稱類型的各種手段
為了方便,下面都用 CNY、USD 幣種來(lái)示例,類型檢查用下面兩個(gè)方法測(cè)試。
function?buyPekingDuck(money:?CNY)?{}?//?只能用?CNY?買北京烤鴨
function?buyCocaCola(money:?USD)?{}?//?只能用?USD?買可口可樂(lè)
為了術(shù)語(yǔ)一致,下文統(tǒng)一用下列中文字詞(若與習(xí)慣的表述不一致,請(qǐng)以英文單詞為準(zhǔn))
Type Annotation: 類型聲明 ? 變量: 類型,比如let yuan: CNYType Assertion: 類型斷言 表達(dá)式 as 類型,比如12 as CNYType Compatibility: 類型兼容,指一個(gè)類型可以賦值給另一個(gè)類型 Type Infer: 類型推斷,指 TS 根據(jù)上下文推斷變量或值的類型,比如 let a = 12推斷a是numberPrimitive Type: 原始類型,指 string,number和boolean
1、定義私有屬性的類 Class with a private property
class?CNY?{
??private?__brand:?void
??constructor(public?value:?number)?{}
}
class?USD?{
??private?__brand:?void
??constructor(public?value:?number)?{}
}
//?用例
const?yuan?=?new?CNY(12)
const?dollar?=?new?USD(5)
//?類型安全
buyPekingDuck(dollar)?//?Argument?of?type?'USD'?is?not?assignable?to?parameter?of?type?'CNY'.
buyCocaCola(yuan)?//?Argument?of?type?'CNY'?is?not?assignable?to?parameter?of?type?'USD'.這個(gè)方法利用了 TS 對(duì) private/protected 的特殊處理——判斷類型兼容時(shí),如果其中一個(gè)包含私有屬性,則另一個(gè)必須包含來(lái)自同一個(gè)類聲明的相同私有屬性。yuan 和 dollar 都有私有屬性__brand,但來(lái)自不同的類聲明(分別是CNY 和 USD),所以它們類型不兼容。
優(yōu)點(diǎn):不需要類型聲明(type annotation),也不需要類型斷言(type assertion),TS 能推導(dǎo)出對(duì)應(yīng)的類型(type infer)。缺點(diǎn):冗余的類聲明,多了一層{ value }的結(jié)構(gòu),不能支持原始類型,需要額外的序列化處理。推薦度:不推薦。除非本來(lái)就是用類實(shí)現(xiàn),而且要嚴(yán)格區(qū)分字段相同、語(yǔ)義不同的兩個(gè)類型,才考慮該方案。
2、包含字面量類型
type?CNY?=?{
??currency:?'CNY',
??value:?number,
}
type?USD?=?{
??currency:?'USD',
??value:?number,
}
//?用例
const?yuan:?CNY?=?{?currency:?'CNY',?value:?12?}
const?dollar:?USD?=?{?currency:?'USD',?value:?5}
//?類型安全
buyPekingDuck(dollar)?//?Argument?of?type?'USD'?is?not?assignable?to?parameter?of?type?'CNY'.
buyCocaCola(yuan)?//?Argument?of?type?'CNY'?is?not?assignable?to?parameter?of?type?'USD'.加入不同的字面量類型(literal type)來(lái)定義 type 或 interface,因?yàn)椴煌置媪渴遣煌愋?,所以組合后的類型也不同。
優(yōu)點(diǎn):語(yǔ)義清晰,理解直觀,條件判斷能實(shí)現(xiàn)類型收窄(type narrowing)。缺點(diǎn):多了一層{ value }的結(jié)構(gòu),不能支持原始類型,需要額外的序列化處理。推薦度:看情況。如果本來(lái)有結(jié)構(gòu),而且用于區(qū)分的字面量有對(duì)應(yīng)的語(yǔ)義,可以用該方法。
3、枚舉類 intersection
enum?CNYBrand?{?_brand?}
type?CNY?=?number?&?CNYBrand
enum?USDBrand?{?_brand?}
type?USD?=?number?&?USDBrand
//?用例
const?yuan?=?12?as?CNY
const?dollar?=?5?as?USD
//?類型安全
buyPekingDuck(dollar)?//?Argument?of?type?'USDBrand'?is?not?assignable?to?parameter?of?type?'CNYBrand'.
buyCocaCola(yuan)?//?Argument?of?type?'CNYBrand'?is?not?assignable?to?parameter?of?type?'USDBrand'.枚舉定義了{ _brand },TS會(huì)認(rèn)為是非空數(shù)字枚舉,兩個(gè)枚舉不兼容,與數(shù)字類型交集后就是不同類型。
注意,字符串不能這么用,string & CNYBrand的結(jié)果是never。枚舉需要定義為{ _brand: ''},讓TS認(rèn)為是非空字符串枚舉,才能跟字符串類型取交集。
優(yōu)點(diǎn):無(wú),勉強(qiáng)要說(shuō)的話,類型斷言的 as Xxx 可讀性還行。缺點(diǎn):需要類型斷言,有額外的枚舉定義,會(huì)生成多余的js代碼,數(shù)字和字符串類型用法不一樣,不支持其他原始類型(布爾類型)。推薦度:不推薦。為了標(biāo)稱類型增加額外運(yùn)行損耗,不值得。
4、unique symbol
type?CNY?=?number?&?{
??readonly?brand:?unique?symbol
}
type?USD?=?number?&?{
??readonly?brand:?unique?symbol
}
//?用例
const?yuan?=?12?as?CNY
const?dollar?=?5?as?USD
//?類型安全
buyPekingDuck(dollar)?//?Argument?of?type?'USD'?is?not?assignable?to?parameter?of?type?'CNY'.
buyCocaCola(yuan)?//?Argument?of?type?'CNY'?is?not?assignable?to?parameter?of?type?'USD'.TS 里每個(gè) unique symbol 聲明都是完全獨(dú)立的唯一標(biāo)識(shí),互相不兼容。作為屬性加到類型中需要用readonly修飾。
優(yōu)點(diǎn):類型定義部分無(wú)差異,不用費(fèi)心思,無(wú)額外的結(jié)構(gòu),運(yùn)行時(shí)無(wú)消耗。缺點(diǎn):需要類型斷言,關(guān)鍵字較多(unique和readonly),不能用范型。推薦度:推薦。不會(huì)生成額外代碼,其唯一性確保類型不會(huì)重復(fù)。
5、brand interface
interface?CNY?extends?Number?{
??_CNYBrand:?string;
}
interface?USD?extends?Number?{
??_USDBrand:?string;
}
//?用例
const?yuan:?CNY?=?12?as?any
const?dollar:?USD?=?5?as?any
//?類型安全
buyPekingDuck(dollar)?//?Argument?of?type?'USD'?is?not?assignable?to?parameter?of?type?'CNY'.
buyCocaCola(yuan)?//?Argument?of?type?'CNY'?is?not?assignable?to?parameter?of?type?'USD'.用 interface 擴(kuò)展增加互不相同的_xxxBrand變成不同的類型,破壞類型兼容。TS 的源碼也使用了該方案。
優(yōu)點(diǎn):支持基本類型,沒(méi)用到黑魔法,無(wú)額外的結(jié)構(gòu),運(yùn)行時(shí)無(wú)消耗。缺點(diǎn):需要類型聲明或類型斷言,且需要過(guò) any 一道。推薦度:非常推薦。大部分需要標(biāo)稱類型的場(chǎng)景不會(huì)直接指定類型,缺點(diǎn)可接受,優(yōu)先考慮該方案。
6、brand type intersection
type?CNY?=?number?&?{
??_CNYBrand:?string;
}
type?USD?=?number?&?{
??_USDBrand:?string;
}
//?用例
const?yuan:?CNY?=?12?as?any
const?dollar:?USD?=?5?as?any
//?類型安全
buyPekingDuck(dollar)?//?Argument?of?type?'USD'?is?not?assignable?to?parameter?of?type?'CNY'.
buyCocaCola(yuan)?//?Argument?of?type?'CNY'?is?not?assignable?to?parameter?of?type?'USD'.同上,只不過(guò) interface extend 改成等價(jià)的 type intersection,即,用類型交集增加互不相同的_xxxBrand變成不同的類型,破壞類型兼容。
優(yōu)點(diǎn):支持基本類型,沒(méi)用到黑魔法,無(wú)額外的結(jié)構(gòu),運(yùn)行時(shí)無(wú)消耗。缺點(diǎn):需要類型聲明或類型斷言,且需要過(guò) any 一道。推薦度:非常推薦。同上,大部分需要標(biāo)稱類型的場(chǎng)景不會(huì)直接指定類型,缺點(diǎn)可接受,優(yōu)先考慮該方案。
上面列舉了社區(qū)常見(jiàn)的標(biāo)稱類型實(shí)現(xiàn)方法,其中個(gè)人最推薦的是 brand interface 以及等價(jià)的 brand type intersection,原理簡(jiǎn)單易懂,沒(méi)有黑魔法,適合絕大多數(shù)使用場(chǎng)景,也是 TS 官方源碼里在用的方法,值得優(yōu)先考慮。
后記
本文簡(jiǎn)單介紹了標(biāo)稱類型是什么,以及 TS 中如何實(shí)現(xiàn)。除了本文提到的這些方法外,網(wǎng)上還能找到很多標(biāo)稱類型的實(shí)現(xiàn)手段,它們各有優(yōu)劣,適用場(chǎng)景也有差異,而且隨著 TS 升級(jí),有些方法已經(jīng)失效了,不熟悉的話可能會(huì)難以抉擇,故沒(méi)有收錄到文章中。
本系列后續(xù)文章會(huì)從實(shí)現(xiàn)原理進(jìn)一步剖析這些方法,了解其背后的機(jī)制,并結(jié)合實(shí)際使用場(chǎng)景來(lái)辨析,爭(zhēng)取知其然,知其所以然。
