阿寶哥精心準備的《輕鬆學 TypeScript》視頻教程已經更新到第九期了,通過形象生動的動畫,讓你輕鬆搞懂 TypeScript 的難點和核心知識點!
你是不是已經掌握 TypeScript 的交叉類型了?如果是的話,你知道這些類型經過交叉運算後的結果麼?如果不清楚的話,閱讀完本文也許你就懂了。
//非對象類型交叉運算typeN0=string&number;typeN1=any&1;typeN2=any&never;//對象類型交叉運算typeA={kind:'a',foo:string};typeB={kind:'b',foo:number};typeC={kind:'c',foo:number};typeAB=A&B;typeBC=B&C;//函數類型交叉運算typeF1=(a:string,b:string)=>void;typeF2=(a:number,b:number)=>void;typeFn=F1&F2在學習 TypeScript 的過程中,你可以把類型理解成一系列值的集合。比如,你可以把數字類型看作是所有數字的集合,1.0、68 就屬於這個集合中,而 "阿寶哥" 就不屬於這個集合,因為它屬於字符串類型。
同樣,對於對象類型來說,我們也可以把它理解成對象的集合。比如以上代碼中 Point 類型表示含有 x 和 y 屬性,且屬性值的類型都是 number 類型對象的集合。而 Named 類型表示含有 name 屬性且屬性值的類型是 string 類型對象的集合。
interfacePoint{x:number;y:number;}interfaceNamed{name:string;}在集合論中,假設 A,B 是兩個集合,由所有屬於集合 A 且屬於集合 B 的元素所組成的集合,叫做集合 A 與集合 B 的交集。
當我們對 Point 類型和 Named 類型進行交集運算,就會產生新的類型。該類型中所包含的對象既屬於 Point 類型,又屬於 Named 類型。
在 TypeScript 中為我們提供了交叉運算符,來實現對多種類型進行交叉運算,所產生的新類型也被稱為交叉類型。
下面我們來簡單介紹一下交叉運算符,該運算符滿足以下這些特性:
在以上代碼中,any 類型和 never 類型比較特殊。除了 never 類型之外,任何類型與 any 類型進行交叉運算的結果都是 any 類型。
介紹完交叉運算符之後,我們來看一下對 Point 類型和 Named 類型進行交叉運算後,將產生什麼樣的類型?
interfacePoint{x:number;y:number;}interfaceNamed{name:string;}typeNamedPoint=Point&Named//{//.x:number;//.y:number;//.name:string;//.}在以上代碼中,新產生的 NamedPoint 類型將會同時包含 x、y 和 name 屬性。但如果進行交叉運算的多個對象類型中,包含相同的屬性但屬性的類型不一致結果又會是怎樣呢?
interfaceX{c:string;d:string;}interfaceY{c:number;e:string}typeXY=X&Y;typeYX=Y&X;在以上代碼中,接口 X 和接口 Y 都含有一個相同的 c 屬性,但它們的類型不一致。對於這種情況,此時 XY 類型或 YX 類型中 c 屬性的類型是不是可以是 string 或 number 類型呢?下面我們來驗證一下:
letp:XY={c:"c",d:"d",e:"e"};//Errorletq:YX={c:6,d:"d",e:"e"};//Error為什麼接口 X 和接口 Y 進行交叉運算後,c 屬性的類型會變成 never 呢?這是因為運算後 c 屬性的類型為 string & number,即 c 屬性的類型既可以是 string 類型又可以是 number 類型。很明顯這種類型是不存在的,所以運算後 c 屬性的類型為 never 類型。
在前面示例中,剛好接口 X 和接口 Y 中 c 屬性的類型都是基本數據類型。那麼如果不同的對象類型中含有相同的屬性,且屬性類型是非基本數據類型的話,結果又會是怎樣呢?我們來看個具體的例子:
interfaceD{d:boolean;}interfaceE{e:string;}interfaceF{f:number;}interfaceA{x:D;}interfaceB{x:E;}interfaceC{x:F;}typeABC=A&B&C;letabc:ABC={//Okx:{d:true,e:'阿寶哥',f:666}};由以上結果可知,在對多個類型進行交叉運算時,若存在相同的屬性且屬性類型是對象類型,那麼屬性會按照對應的規則進行合併。
但需要注意的是,在對對象類型進行交叉運算的時候,如果對象中相同的屬性被認為是可辨識的屬性,即屬性的類型是字面量類型或字面量類型組成的聯合類型,那麼最終的運算結果將是 never 類型:
typeA={kind:'a',foo:string};typeB={kind:'b',foo:number};typeC={kind:'c',foo:number};typeAB=A&B;//nevertypeBC=B&C;//never在以上代碼中,A、B、C 三種對象類型都含有 kind 屬性且屬性的類型都是字符串字面量類型,所以 AB 類型和 BC 類型最終都是 never 類型。接下來,我們來繼續看個例子:
typeFoo={name:string,age:number}typeBar={name:number,age:number}typeBaz=Foo&Bar//{//.name:never;//.age:number;//}在以上代碼中,Baz 類型是含有 name 屬性和 age 屬性的對象類型,其中 name 屬性的類型是 never 類型,而 age 屬性的類型是 number 類型。
但如果把 Foo 類型中 name 屬性的類型改成 boolean 類型的話,Baz 類型將會變成 never 類型。這是因為 boolean 類型可以理解成由 true 和 false 字面量類型組成的聯合類型。
typeFoo={name:boolean,//true|falseage:number}typeBar={name:number,age:number}typeBaz=Foo&Bar//never其實除了對象類型可以進行交叉運算外,函數類型也可以進行交叉運算:
typeF1=(a:string,b:string)=>void;typeF2=(a:number,b:number)=>void;letf:F1&F2=(a:string|number,b:string|number)=>{};f("hello","world");//Okf(1,2);//Okf(1,"test");//Error對於以上代碼中的函數調用語句,只有 f(1, "test") 的調用語句會出現錯誤,其對應的錯誤信息如下:
沒有與此調用匹配的重載。第1個重載(共2個),「(a:string,b:string):void」,出現以下錯誤。類型「number」的參數不能賦給類型「string」的參數。第2個重載(共2個),「(a:number,b:number):void」,出現以下錯誤。類型「string」的參數不能賦給類型「number」的參數。ts(2769)根據以上的錯誤信息,我們可以了解到 TypeScript 編譯器會利用函數重載的特性來實現不同函數類型的交叉運算,要解決上述問題,我們可以在定義一個新的函數類型 F3,具體如下:
typeF1=(a:string,b:string)=>void;typeF2=(a:number,b:number)=>void;typeF3=(a:number,b:string)=>void;letf:F1&F2&F3=(a:string|number,b:string|number)=>{};f("hello","world");//Okf(1,2);//Okf(1,"test");//Ok掌握了交叉類型之後,在結合往期文章中介紹的映射類型,我們就可以根據工作需要實現一些自定義工具類型了。比如實現一個 PartialByKeys 工具類型,用於把對象類型中指定的 keys 變成可選的。
typeUser={id:number;name:string;age:number;}typePartialByKeys<T,KextendskeyofT>=Simplify<{[PinK]?:T[P]}&Pick<T,Exclude<keyofT,K>>>typeU1=PartialByKeys<User,"id">typeU2=PartialByKeys<User,"id"|"name">那麼如果讓你實現一個 RequiredByKeys 工具類型,用於把對象類型中指定的 keys 變成必填的,你知道怎麼實現麼?知道答案的話,可以在評論區留言喲。你喜歡以這種形式學 TS 麼?喜歡的話,記得點讚與收藏喲。
掃碼查看輕鬆學 TypeScript系列視頻教程
(目前已更新9期)