TypeScript 類型系統(tǒng):分布式條件類型全解
本文是對在極客時間 與 早早聊 的直播 中提到的 分布式條件類型 的進(jìn)一步說明,但你不需要已經(jīng)觀看過相關(guān)直播,本文會包括前置知識部分。
注意:本文的重點是分布式條件類型,對于前置的條件類型部分不會有特別深入的講解,但其實也夠了。
本文的主要內(nèi)容仍然主要來自于對之前的 TypeScript的另一面:類型編程(2021重制版) 一文中,對分布式條件類型的講解,也推薦仍處于學(xué)習(xí)階段的同學(xué)完整的閱讀這篇文章,我敢保證看完你的 TypeScript 水平就已經(jīng)超越 79.8% 的使用者了。
條件類型
在本文中,我們只會介紹 extends 語句在常見情況下的成立情況,其他與條件類型相關(guān)的知識如原理、infer 關(guān)鍵字、基于條件類型的泛型約束等不做介紹(因為和本文主旨并冇關(guān)系)。
常見的 extends 語句可以分成這么幾種成立情況:
字面量類型及其原始類型 基類與派生類的子類型關(guān)系 基于結(jié)構(gòu)化類型系統(tǒng)的子類型關(guān)系 聯(lián)合類型與其分支的子類型關(guān)系 Top Type 與 Bottom Type 分布式條件類型
我們會一一介紹。
字面量類型及其原始類型
字面量類型包括數(shù)字字面量、字符串字面量以及布爾字符串類型,它們是比原始類型更有意義的類型信息,如我們可以標(biāo)注一個可選值確定的返回值:
一般認(rèn)為模板字符串類型近似于字面量類型。
type?ResCode?=?200?|?400?|?500;
我們可以標(biāo)注一個確定的字符串集合:
interface?Foo?{
??status:?'success'?|?'failure'
}
也可以混用:
type?Mixed?=?true?|?599?|?'Linbudu'
本質(zhì)上,對于字面量類型,其可以理解為它是對原始類型的進(jìn)一步收斂,是比原始類型更精確的類型,因此對于 extends 語句其必然成立。
//?true
type?_T1?=?'linbudu'?extends?string???true?:?false;
從另一種角度來說,我們也可以認(rèn)為字面量類型其實就是繼承自原始類型,如以下偽代碼:
class?LinbuduLiteralType?extends?String?{
??public?value?=?'Linbudu';
}
子類型關(guān)系
對于基類和派生類的子類型關(guān)系,這一部分不做解釋,直接上代碼:
class?Base?{
??name!:?string;
}
class?Derived?extends?Base?{
??age!:?number;
}
//?true
type?_T1?=?Derived?extends?Base???true?:?false;
還存在著一種類似的情況,即結(jié)構(gòu)化類型系統(tǒng)判斷得到的子類型關(guān)系:
關(guān)于結(jié)構(gòu)化類型系統(tǒng)和標(biāo)稱類型系統(tǒng)的差異,咱們以后再說。
type?_T2?=?{?name:?'linbudu';?}?extends?Base
????true
??:?false;
type?_T3?=?{?name:?'linbudu';?age:?18;?job:?'engineer'?}?extends?Base
????true
??:?false;
在這里我們手動定義的對象并沒有真的 extends Base Class,但由于其內(nèi)部的屬性與 Base 類型一致(_T3 ?還額外進(jìn)行了擴展),結(jié)構(gòu)化類型系統(tǒng)通過比較內(nèi)部的屬性與屬性類型來判斷類型的兼容性,因此這里 extends 同樣成立。
還有一種更特殊的情況,即對于空對象{} 的比較:
type?_T4?=?{}?extends?{}
????true
??:?false;
type?_T5?=?{?name:?'linbudu';??}?extends?{}
????true
??:?false;
??
type?_T6?=?string?extends?{}
????true
??:?false;
本質(zhì)上類似于基類以及派生類,但空對象由于其內(nèi)部無屬性,任意一個對象(甚至是原始類型)都可以認(rèn)為是它的子集。
聯(lián)合類型及其分支
對于聯(lián)合類型的比較,其實就是比較前者的類型分支在后者中是否都存在,或者說前者是否是后者的子集:
//?true
type?_T7?=?'a'?extends?'a'?|?'b'?|?'c'???true?:?false;
//?true
type?_T8?=?'a'?|?'b'?extends?'a'?|?'b'?|?'c'???true?:?false;
//?false
type?_T9?=?'a'?|?'b'?|?'wuhu!'?extends?'a'?|?'b'?|?'c'???true?:?false;
在分布式條件類型一節(jié)中,我們會了解更多。
Top Type 與 Bottom Type
是的,這又是一個獨立的知識點,我們還是以后... 所以你知道 TypeScript 類型系統(tǒng)的知識有多重要了吧。
在 TypeScript 我們說 any 與 unknown 是 Top Type,而 never 則是 Bottom Type。Top Type 意味著它們處于類型層級的頂端,即任意的類型都屬于其子類型,對于 OtherType extends any 與 OtherType extends unknown 必定成立。而 Bottom Type 處于類型層級的底端,意味著無法再細(xì)分的類型,除了 never 自身,沒有別的類型能夠再賦值給它。這也就意味著,它屬于任意類型的子類型,即 never extends OtherType 必定成立。
那么,一環(huán)一環(huán)的套起來,我們就可以構(gòu)造出一條 extends 鏈,來直觀地分析 TypeScript 的類型層級了:
//?8,即所有?extends?均成立
type?_Chain?=?never?extends?'linbudu'
????'linbudu'?extends?'linbudu'?|?'budulin'
??????'linbudu'?extends?string
????????string?extends?{}
??????????{}?extends?Object
????????????Object?extends?any
??????????????Object?extends?unknown
????????????????any?extends?unknown
??????????????????unknown?extends?any
????????????????????8
??????????????????:?7
????????????????:?6
??????????????:?5
????????????:?4
??????????:?3
????????:?2
??????:?1
????:?0
??:?never;
分布式條件類型
分布式條件類型(Distributive Conditional Types)是 TypeScript 中條件類型的特殊功能之一,因此也被稱為條件類型的分布式特性。
對于它其實沒有什么特別晦澀難懂的彎彎繞繞,就是滿足一定條件后必然會發(fā)生的事情罷了,就好像餓了要吃飯,困了要睡覺一樣。所以沒必要也不應(yīng)該敬畏它,看我怎么把它扒光在你們面前。
來看一個例子,對于內(nèi)置工具類型 Exclude 的使用:
type?Extract?=?T?extends?U???T?:?never;
interface?IObject?{
??a:?string;
??b:?number;
??c:?boolean;
}
//?'a'|'b'
type?_ExtractedKeys1?=?Extract'a'|'b'>;
//?'a'|'b'
type?_ExtractedKeys2?=?Extract<'a'|'b'|'c',?'a'|'b'>;
//?never
type?_ExtractedKeys3?=?'a'|'b'|'c'?extends?'a'|'b'???'a'|'b'|'c'?:?never;
本質(zhì)上,這三個類型別名執(zhí)行的代碼是一模一樣的,但是為什么 3 和 1、2 表現(xiàn)得不一致(雖然這個 extends 不成立我們是理解的)?
研究下兩種情況的區(qū)別,你會發(fā)現(xiàn) 1、2 是通過傳入給泛型參數(shù),再由泛型參數(shù)進(jìn)行判斷的,而 3 則是直接使用聯(lián)合類型進(jìn)行的判斷。
記住第一個差異:是否作為泛型參數(shù)。
再看一個例子:
type?Naked?=?T?extends?boolean???"Y"?:?"N";
type?Wrapped?=?[T]?extends?[boolean]???"Y"?:?"N";
//?"N"?|?"Y"
type?Result1?=?Naked<number?|?boolean>;
//?"N"
type?Result2?=?Wrapped<number?|?boolean>;
這里倒是都通過泛型參數(shù)進(jìn)行判斷了,但結(jié)果又不一樣了,為什么第一個得到了聯(lián)合類型?第二個倒是能理解,[1, true] 肯定和 [true, true] 不兼容嘛。
對于第一個,你可能已經(jīng)發(fā)現(xiàn)這里作為結(jié)果的聯(lián)合類型,恰好對應(yīng)上了分別使用 number、boolean 判斷結(jié)果,就好像于謙的父親也姓于一樣這么巧?那為什么第二個沒有被這樣子拆開比較?
記住第二個差異:泛型參數(shù)在條件類型是否被數(shù)組包裹了。
其實這里的兩個差異就是分布式條件類型要發(fā)生的前提條件:
首先,你得是聯(lián)合類型 其次,你的聯(lián)合類型需要是通過泛型參數(shù)的形式傳入 最后,你的泛型參數(shù)在條件類型語句中需要是裸類型參數(shù),即沒有被 [] 包裹
合起來,我們就得到了官方的解釋:對于屬于裸類型參數(shù)的檢查類型,條件類型會在實例化時期自動分發(fā)到聯(lián)合類型上。
Conditional types in which the checked type is a naked type parameter are called distributive conditional types. Distributive conditional types are automatically distributed over union types during instantiation.
現(xiàn)在我們可以講講這里的分布式代表啥了,第一個例子中實際上是這么一個判斷過程:
//?('a'?extends?'a'|'b')?|?('b'?extends?'a'|'b')?|?('c'?extends?'a'|'b')
//?'a'|'b'|never
//?'a'|'b'
type?_ExtractedKeys1?=?Extract'a'|'b'>;
即聯(lián)合類型的分支被單獨的拿出來依次進(jìn)行條件類型語句的判斷。類似的,第二個例子在 Naked 工具類型也會將傳入的聯(lián)合類型參數(shù)進(jìn)行分發(fā)判斷,而 Wrapped 由于不滿足裸類型參數(shù)的條件,導(dǎo)致分布式不成立,因此不會進(jìn)行分發(fā)。
你可能會好奇,為啥要專門設(shè)計分布式條件類型這個東西?實際上,可以說它也是 TypeScript 類型系統(tǒng)的基石之一,大量的工具類型底層都依賴了它來進(jìn)行聯(lián)合類型的過濾,然后基于聯(lián)合類型的過濾去做映射類型、Pick/Omit 這一類接口裁剪操作,準(zhǔn)備開始實戰(zhàn)環(huán)節(jié)!
分布式條件類型的應(yīng)用
TypeScript 內(nèi)置了幾個基于分布式條件類型的工具類型:
type?Extract?=?T?extends?U???T?:?never;
type?Exclude?=?T?extends?U???never?:?T;
type?NonNullable?=?T?extends?null?|?undefined???never?:?T;
它們的作用分別是:
Extract,提取 集合T 中也存在于 集合U 中的類型分支,即 T 與 U 的交集 Exclude,提取 集合T 中不存在于 集合U 中的類型分支,即 T 相對于 U 的差集 NonNullable,移除集合中的 null 與 undefined
它們的工作機理就是分布式條件類型了,看到了差集與交集,你的數(shù)學(xué) DNA 是否動了?
當(dāng)然,我們可以很容易得實現(xiàn)并集與補集:
export?type?Concurrence?=?A?|?B;
export?type?Complementextends?A>?=?Exclude;
勉為其難畫個圖好了,我可不是隨便畫圖的人。

唯一需要注意的就是補集,補集實際上是特殊情況的差集,即 U 為 T 的子集,此時有 T 相對于 U 的差集 + U = T。
這里我們實現(xiàn)了普通集合的情況,如果眉頭一皺,你會發(fā)現(xiàn),問題并不簡單。如果我們面臨的是對象的情況呢?這個時候如果我們?nèi)ο蟛⒓蔷蜎]那么簡單了,比如,如果一個鍵在兩個對象中都存在,那么以哪個對象的鍵值類型為準(zhǔn)?又比如,如果我們想實現(xiàn)合并對象時,只使用新對象的鍵值類型覆蓋掉原對象的同鍵鍵值類型,但不想合并新的鍵過來?
這其實就是類型編程中,我稱之為一刀兩端藕斷絲連再續(xù)前緣重歸于好的操作,其實就是把一個接口,拆成多個部分,對某一部分做處理,然后再合并。這一思想在許多工具類型中都有體現(xiàn),如:
export?type?MarkPropsAsOptional<
??T?extends?Record<string,?any>,
??K?extends?keyof?T?=?keyof?T
>?=?Omit?&?Partial>;
這個工具類型的作用是將接口中指定的一部分變?yōu)榭蛇x,而不像 Partial 那樣全量的變?yōu)榭蛇x。它的思路就是把接口拆成 保持不變的 + 需要標(biāo)記為可選的,然后對后一部分應(yīng)用 Partial 類型,再合并即可。
回到對象的覆蓋,其實思路是一樣的。
合并 T 和 U 的所有鍵值對,以 U 的鍵值類型優(yōu)先級更高 T 比 U 多的部分:T 相對于 U 的差集 U 比 T 多的部分:U 相對于 T 的差集 T 與 U 的交集,通過 Extract,將 T 視為后入集合,實現(xiàn)以 U 為主集合,即 U 的類型優(yōu)先級更高。
在開始前,我們還需要來實現(xiàn)對象交集、對象差集等幾個輔助工具類型,以及輔助它們的對象鍵集合交集、對象鍵集合差集的輔助輔助工具類型(笑。為了方便理解,我們把 Extract 命名為 Intersection, Exclude 命名為 Difference。
type?PlainObjectType?=?Record<string,?any>;
export?type?Intersection?=?A?extends?B???A?:?never;
export?type?Difference?=?A?extends?B???never?:?A;
export?type?ObjectKeysIntersection<
??T?extends?PlainObjectType,
??U?extends?PlainObjectType
>?=?Intersection?&?Intersection;
export?type?ObjectKeysDifference<
??T?extends?PlainObjectType,
??U?extends?PlainObjectType
>?=?Difference;
export?type?ObjectIntersection<
??T?extends?PlainObjectType,
??U?extends?PlainObjectType
>?=?Pick>;
export?type?ObjectDifference<
??T?extends?PlainObjectType,
??U?extends?PlainObjectType
>?=?Pick>;
這樣就可以實現(xiàn)以新對象類型優(yōu)先級更高的 Merge 了:
type?Merge<
??T?extends?PlainObjectType,
??U?extends?PlainObjectType
??//?T?比?U?多的部分,加上?T?與?U?交集的部分(類型不同則以?U?優(yōu)先級更高,再加上?U?比?T?多的部分即可
>?=?ObjectDifference?&?ObjectIntersection?&?ObjectDifference;
類似的,如果要保證原對象類型優(yōu)先級更高,反轉(zhuǎn)下交集即可:
type?Assign<
??T?extends?PlainObjectType,
??U?extends?PlainObjectType
??//?T?比?U?多的部分,加上?T?與?U?交集的部分(類型不同則以?T?優(yōu)先級更高,再加上?U?比?T?多的部分即可
>?=?ObjectDifference?&?ObjectIntersection?&?ObjectDifference;
擴展
& 與 |
可能前面有的同學(xué)注意到了,我們的普通集合交集使用的是 | ,但對象的交集使用的是交叉類型 &:
export?type?Concurrence?=?A?|?B;
//?此?IntersectionTypes?非彼?Intersection
//?懶得寫約束了,反正都得是對象類型就是了,西內(nèi)!
export?type?IntersectionTypes?=?T?&?U?&?K;
這是因為,只有對于對象類型,& 才表現(xiàn)為合并的情況,而對于原始類型以及聯(lián)合類型,& 就真的表現(xiàn)為,交集。
//?'a'
type?_T1?=?('a'?|?'b')?&?('a'?|?'d'?|?'e'?|?'f')
//?never,因為?string?和?number?哪有交集啊
type?_T1?=?string?&?number;
家庭作業(yè)
小明想要把兩個對象 A、B 合并在一起,但不想要對象 B 中比對象 A 多的部分,而是只想把 B 中同鍵的鍵值類型合并過來,你能幫幫他嗎?
