浅谈 TS 标称类型介绍及社区实现
本文將以稍偏門的視角來看待 TypeScript 的類型系統,主要介紹標簽類型是什么,以及 TS 社區都有哪些實現手段。
前言
有位大神說過"程序是類型的證明",我看不懂,但我大受震撼。為了以后能看懂哪怕一點點,我決定記錄下類型相關的所學所悟。
《淺談 TS 標稱類型》系列將以稍偏門的視角來看待 TypeScript 的類型系統,實際用途不大,但自覺有趣。本文是該系列的開篇文章,主要介紹標簽類型是什么,以及 TS 社區都有哪些實現手段。
什么是標稱類型系統(nominal type system)
先通俗地理解下,舉個例子,userId = 123、bookId = 34都是數字,但兩者用于不同的場景,希望用不同類型?UserID?和?BookID?來表示,且不能互換。像這樣,數據的值本身沒什么區別,安上不同名字就是不同類型,這就是標稱類型系統(nominal type system)。也就是說,標稱類型系統中,兩個變量是否類型兼容(可以交換賦值)取決于這兩個變量顯式聲明的類型名字是否相同。
與之相對的是結構類型系統(structural type system),類型兼容只取決于實際結構是否相同,與類型名字無關。比如:定義Point類型包含x、y兩個數字,rect = { x: 33, y: 3, width: 30, height: 80 }的結構滿足Point的定義,就屬于Point類型。簡單理解,結構類型系統中,結構或者說形狀相同的兩個值,它們的類型是兼容的,可以交換賦值。
更嚴格的定義可以看下Type system - Wikipedia的說明。
除了上面的?UserID?和?BookID?的例子,標稱類型還有其他常見的應用場景,比如:區分不同的字符串(正則表達式、html模版、文件路徑等),表達不同單位的量綱(不同幣種的金額、css各種長度單位)等。這些會在后續文章再展開說明,屆時也會列舉下標稱類型常見的錯誤用法。
TS 是標稱類型系統嗎
不是。TS 是結構類型系統(structural type system),基于結構/形狀檢查類型,而非類型的名字。
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官方文檔的說明,里面還舉了一些例子,可以先看看加深理解。
TS 可以實現標稱類型嗎
可以(不然這篇文章寫到這里就要結束了)。TS 目前不支持顯式聲明標稱類型,也沒有計劃支持,2014年的提案Support some non-structural (nominal) type matching · Issue #202 到現在還是Open狀態。不過社區有不少方案,可以基于現有 TS 的能力一定程度上實現標稱類型,整理如下。
TS 實現標稱類型的各種手段
為了方便,下面都用 CNY、USD 幣種來示例,類型檢查用下面兩個方法測試。
function buyPekingDuck(money: CNY) {} // 只能用 CNY 買北京烤鴨 function buyCocaCola(money: USD) {} // 只能用 USD 買可口可樂為了術語一致,下文統一用下列中文字詞(若與習慣的表述不一致,請以英文單詞為準)
Type Annotation:?類型聲明??變量: 類型?,比如?let yuan: CNY
Type Assertion:?類型斷言?表達式 as 類型,比如?12 as CNY
Type Compatibility:?類型兼容,指一個類型可以賦值給另一個類型
Type Infer:?類型推斷,指 TS 根據上下文推斷變量或值的類型,比如?let a = 12?推斷?a?是?number
Primitive Type:?原始類型,指string,?number?和?boolean
???定義私有屬性的類 Class with a private property
這個方法利用了 TS 對?private/protected?的特殊處理——判斷類型兼容時,如果其中一個包含私有屬性,則另一個必須包含來自同一個類聲明的相同私有屬性。yuan?和?dollar?都有私有屬性__brand,但來自不同的類聲明(分別是CNY?和?USD),所以它們類型不兼容。
優點:不需要類型聲明(type annotation),也不需要類型斷言(type assertion),TS 能推導出對應的類型(type infer)。
缺點:冗余的類聲明,多了一層{ value }的結構,不能支持原始類型,需要額外的序列化處理。
推薦度:不推薦。除非本來就是用類實現,而且要嚴格區分字段相同、語義不同的兩個類型,才考慮該方案。
???包含字面量類型
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)來定義 type 或 interface,因為不同字面量是不同類型,所以組合后的類型也不同。
優點:語義清晰,理解直觀,條件判斷能實現類型收窄(type narrowing)。
缺點:多了一層{ value }的結構,不能支持原始類型,需要額外的序列化處理。
推薦度:看情況。如果本來有結構,而且用于區分的字面量有對應的語義,可以用該方法。
???枚舉類intersection
enum?CNYBrand?{?_brand?} type?CNY?=?number?&?CNYBrandenum?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會認為是非空數字枚舉,兩個枚舉不兼容,與數字類型交集后就是不同類型。
注意,字符串不能這么用,string & CNYBrand的結果是never。枚舉需要定義為{ _brand: ''},讓TS認為是非空字符串枚舉,才能跟字符串類型取交集。
優點:無,勉強要說的話,類型斷言的?as Xxx?可讀性還行。
缺點:需要類型斷言,有額外的枚舉定義,會生成多余的js代碼,數字和字符串類型用法不一樣,不支持其他原始類型(布爾類型)。
推薦度:不推薦。為了標稱類型增加額外運行損耗,不值得。
???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?里每個?unique symbol?聲明都是完全獨立的唯一標識,互相不兼容。作為屬性加到類型中需要用readonly修飾。
優點:類型定義部分無差異,不用費心思,無額外的結構,運行時無消耗。
缺點:需要類型斷言,關鍵字較多(unique和readonly),不能用范型。
推薦度:推薦。不會生成額外代碼,其唯一性確保類型不會重復。
???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 擴展增加互不相同的_xxxBrand變成不同的類型,破壞類型兼容。TS 的源碼也使用了該方案。
優點:支持基本類型,沒用到黑魔法,無額外的結構,運行時無消耗。
缺點:需要類型聲明或類型斷言,且需要過 any 一道。
推薦度:非常推薦。大部分需要標稱類型的場景不會直接指定類型,缺點可接受,優先考慮該方案。
???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'.同上,只不過 interface extend 改成等價的 type intersection,即,用類型交集增加互不相同的_xxxBrand變成不同的類型,破壞類型兼容。
優點:支持基本類型,沒用到黑魔法,無額外的結構,運行時無消耗。
缺點:需要類型聲明或類型斷言,且需要過 any 一道。
推薦度:非常推薦。同上,大部分需要標稱類型的場景不會直接指定類型,缺點可接受,優先考慮該方案。
上面列舉了社區常見的標稱類型實現方法,其中個人最推薦的是?brand interface?以及等價的?brand type intersection,原理簡單易懂,沒有黑魔法,適合絕大多數使用場景,也是 TS 官方源碼里在用的方法,值得優先考慮。
后記
本文簡單介紹了標稱類型是什么,以及 TS 中如何實現。除了本文提到的這些方法外,網上還能找到很多標稱類型的實現手段,它們各有優劣,適用場景也有差異,而且隨著 TS 升級,有些方法已經失效了,不熟悉的話可能會難以抉擇,故沒有收錄到文章中。
本系列后續文章會從實現原理進一步剖析這些方法,了解其背后的機制,并結合實際使用場景來辨析,爭取知其然,知其所以然。
團隊介紹
大淘寶技術—行業工作臺前端團隊,有一群熱愛技術,期望用技術推動業務的小伙伴。服務于數千運營和千萬商家,打造高效、穩定、好用的下一代數智化操作系統,讓運營&商家能更輕松、更快捷,給消費者更好的購物體驗。
團隊在建設和探索的核心技術有:新一代無代碼研發產品Orca,包括需求結構化、領域物料、無代碼搭建、服務標準化&編排等細分技術方向;安全生產&用戶體驗產品盾山,包括監控預警、自動化測試、代碼掃描、灰度發布、問題診斷、體驗分析等方向;數據化運營技術,包括數據可視化、數據指標分析、策略驅動運營等技術探索方向。
期待一起參與加入行業工作臺的建設~
???拓展閱讀?
作者|亦森
出品|阿里巴巴新零售淘系技術
總結
以上是生活随笔為你收集整理的浅谈 TS 标称类型介绍及社区实现的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 什么是标称属性?什么是二元属性?什么是序
- 下一篇: huan