因?yàn)?TypeScript 中有靜態(tài)類型檢測眼俊,所以我們?cè)僖膊挥孟?JavaScript 中那樣厢漩,賦給變量任意類型的值待错。
在 TypeScript 中褐澎,能不能把一個(gè)類型賦值給其他類型是由類型兼容性決定的会钝。
特例
首先,我們回顧一下 any、never迁酸、unknown 等特殊類型先鱼,它們?cè)陬愋图嫒菪陨鲜钟刑厣?/p>
(1)any
萬金油 any 類型可以賦值給除了 never 之外的任意其他類型,反過來其他類型也可以賦值給 any奸鬓。也就是說 any 可以兼容除 never 之外所有的類型焙畔,同時(shí)也可以被所有的類型兼容(即 any 既是 bottom type,也是 top type)串远。因?yàn)?any 太特殊宏多,這里我就不舉例子了。
再次強(qiáng)調(diào):Any is 魔鬼澡罚,我們一定要慎用绷落、少用。
(2)never
never 的特性是可以賦值給任何其他類型始苇,但反過來不能被其他任何類型(包括 any 在內(nèi))賦值(即 never 是 bottom type)。比如以下示例中的第 5~7 行筐喳,我們可以把 never 賦值給 number催式、函數(shù)、對(duì)象類型避归。
{
? let never: never = (() => {
? ? throw Error('never');
? })();
? let a: number = never; // ok
? let b: () => any = never; // ok
? let c: {} = never; // ok
}
(3)unknown
unknown 的特性和 never 的特性幾乎反過來荣月,即我們不能把 unknown 賦值給除了 any 之外任何其他類型,反過來其他類型都可以賦值給 unknown(即 unknown 是 top type)梳毙。比如以下示例中的第 3~5 行提示了一個(gè) ts(2322) unknown 類型不能賦值給其他任何類型的錯(cuò)誤哺窄。
{
? let unknown: unknown;
? const a: number = unknown; // ts(2322)
? const b: () => any = unknown; // ts(2322)
? const c: {} = unknown; // ts(2322)
}
(4)void、null账锹、undefined
void萌业、null、undefined 這三大廢材類型的兼容性也很特別奸柬,比如 void 類型僅可以賦值給 any 和 unknown 類型(下面示例第 9~10 行)生年,反過來僅 any、never廓奕、undefined 可以賦值給 void(下面示例第 11~13 行)抱婉。
{
? let thisIsAny: any;
? let thisIsNever: never;
? let thisIsUnknown: unknown;
? let thisIsVoid: void;
? let thisIsUndefined: undefined;
? let thisIsNull: null;
? thisIsAny = thisIsVoid; // ok
? thisIsUnknown = thisIsVoid; // ok
? thisIsVoid = thisIsAny; // ok
? thisIsVoid = thisIsNever; // ok
? thisIsVoid = thisIsUndefined; // ok
? thisIsAny = thisIsNull; // ok
? thisIsUnknown = thisIsNull; // ok
? thisIsAny = thisIsUndefined; // ok
? thisIsUnknown = thisIsUndefined; // ok
? thisIsNull = thisIsAny; // ok
? thisIsNull = thisIsNever; // ok
? thisIsUndefined = thisIsAny; // ok
? thisIsUndefined = thisIsNever; // ok
}
在我們推崇并使用的嚴(yán)格模式下,null桌粉、undefined 表現(xiàn)出與 void 類似的兼容性蒸绩,即不能賦值給除 any 和 unknown 之外的其他類型,反過來其他類型(除了 any 和 never 之外)都不可以賦值給 null 或 undefined铃肯。
(5)enum
最后一個(gè)特例是 enum 枚舉類型患亿,其中數(shù)字枚舉和數(shù)字類型相互兼容。
在如下示例中缘薛,我們?cè)诘?5 行把枚舉 A 賦值給了數(shù)字(number)類型窍育,并在第 7 行使用數(shù)字字面量 1 替代了枚舉 A卡睦。
{
? enum A {
? ? one
? }
? let num: number = A.one; // ok
? let fun = (param: A) => void 0;
? fun(1); // ok
}
此外,不同枚舉之間不兼容漱抓。如下示例中的第 10~11 行表锻,因?yàn)槊杜e A 和 B 不兼容,所以都會(huì)提示一個(gè) ts(2322) 類型的錯(cuò)誤乞娄。
{
? enum A {
? ? one
? }
? enum B {
? ? one
? }
? let a: A;
? let b: B;
? a = b; // ts(2322)
? b = a; // ts(2322)
}
類型兼容性
除了前邊提到的所有特例瞬逊,TypeScript 中類型的兼容性都是基于結(jié)構(gòu)化子類型的一般原則進(jìn)行判定的。
下面我們從結(jié)構(gòu)化類型和子類型這兩方面了解一下一般原則仪或。
(1)子類型
從子類型的角度來看确镊,所有的子類型與它的父類型都兼容,如下代碼所示:
{
? const one = 1;
? let num: number = one; // ok
? interface IPar {
? ? name: string;
? }
? interface IChild extends IPar {
? ? id: number;
? }
? let Par: IPar;
? let Child: IChild;
? Par = Child; // ok
? class CPar {
? ? cname = '';
? }
? class CChild extends CPar {
? ? cid = 1;
? }
? let ParInst: CPar;
? let ChildInst: CChild;
? ParInst = ChildInst; // ok
? let mixedNum: 1 | 2 | 3 = one; // ok
}
在示例中的第 3 行范删,我們可以把類型是數(shù)字字面量類型的 one 賦值給數(shù)字類型的 num蕾域。在第 12 行,我們可以把子接口類型的變量賦值給 Par到旦。在第 21 行旨巷,我們可以把子類實(shí)例 ChildInst 賦值給 ParInst。
因?yàn)槌蓡T類型兼容它所屬的類型集合(其實(shí)聯(lián)合類型和枚舉都算類型集合添忘,這里主要說的是聯(lián)合類型)采呐,所以在示例中的第 22 行,我們可以把 one 賦值給包含字面類型 1 的聯(lián)合類型搁骑。
舉一反三斧吐,由子類型組成的聯(lián)合類型也可以兼容它們父類型組成的聯(lián)合類型,如下代碼所示:
? let ICPar: IPar | CPar;
? let ICChild: IChild | CChild;
? ICPar = ICChild; // ok
在示例中的第 3 行仲器,因?yàn)?IChild 是 IPar 的子類煤率,CChild 是 CPar 的子類,所以 IChild | CChild 也是 IPar | CPar 的子類乏冀,進(jìn)而 ICChild 可以賦值給 ICPar涕侈。
(2)結(jié)構(gòu)類型
類型兼容性的另一準(zhǔn)則是結(jié)構(gòu)類型,即如果兩個(gè)類型的結(jié)構(gòu)一致煤辨,則它們是互相兼容的裳涛。比如擁有相同類型的屬性、方法的接口類型或類众辨,則可以互相賦值端三。
下面我們看一個(gè)具體的示例:
{
? class C1 {
? ? name = '1';
? }
? class C2 {
? ? name = '2';
? }
? interface I1 {
? ? name: string;
? }
? interface I2 {
? ? name: string;
? }
? let InstC1: C1;
? let InstC2: C2;
? let O1: I1;
? let O2: I2;
? InstC1 = InstC2; // ok
? O1 = O2; // ok
? InstC1 = O1; // ok
? O2 = InstC2; // ok
}
因?yàn)轭?C1、類 C2鹃彻、接口類型 I1郊闯、接口類型 I2 的結(jié)構(gòu)完全一致,所以在第 18~19 行我們可以把類 C2 的實(shí)例 InstC2 賦值給類 C1 的實(shí)例 Inst1,把接口類型 I2 的變量 O2 賦值給接口類型 I1 的變量 O1团赁。
在第 20~21 行育拨,我們甚至可以把接口類型 I1 的變量 O1 賦值給類 C1 的實(shí)例,類 C2 的實(shí)例賦值給接口類型 I2 的變量 O2欢摄。
另外一個(gè)特殊的場景:兩個(gè)接口類型或者類熬丧,如果其中一個(gè)類型不僅擁有另外一個(gè)類型全部的屬性和方法,還包含其他的屬性和方法(如同繼承自另外一個(gè)類型的子類一樣)怀挠,那么前者是可以兼容后者的析蝴。
下面我們看一個(gè)具體的示例:
{
? interface I1 {
? ? name: string;
? }
? interface I2 {
? ? id: number;
? ? name: string;
? }
? class C2 {
? ? id = 1;
? ? name = '1';
? }
? let O1: I1;
? let O2: I2;
? let InstC2: C2;
? O1 = O2;
? O1 = InstC2;
}
在示例中的第 16~17 行,我們可以把類 C2 的實(shí)例 InstC2 和接口類型 I2 的變量 O2 賦值給接口類型 I1 的變量 O1绿淋,這是因?yàn)轭?C2闷畸、接口類型 I2 和接口類型 I1 的 name 屬性都是 string。不過吞滞,因?yàn)樽兞?O2佑菩、類 C2 都包含了額外的屬性 id,所以我們不能把變量 O1 賦值給實(shí)例 InstC2裁赠、變量 O2倘待。
這里涉及一個(gè)需要特別注意的特性:雖然包含多余屬性 id 的變量 O2 可以賦值給變量 O1,但是如果我們直接將一個(gè)與變量 O2 完全一樣結(jié)構(gòu)的對(duì)象字面量賦值給變量 O1组贺,則會(huì)提示一個(gè) ts(2322) 類型不兼容的錯(cuò)誤(如下示例第 2 行),這就是對(duì)象字面的 freshness 特性祖娘。
也就是說一個(gè)對(duì)象字面量沒有被變量接收時(shí)失尖,它將處于一種 freshness 新鮮的狀態(tài)。這時(shí) TypeScript 會(huì)對(duì)對(duì)象字面量的賦值操作進(jìn)行嚴(yán)格的類型檢測渐苏,只有目標(biāo)變量的類型與對(duì)象字面量的類型完全一致時(shí)掀潮,對(duì)象字面量才可以賦值給目標(biāo)變量,否則會(huì)提示類型錯(cuò)誤琼富。
當(dāng)然仪吧,我們也可以通過使用變量接收對(duì)象字面量或使用類型斷言解除 freshness,如下示例:
? O1 = {
? ? id: 2, // ts(2322)
? ? name: 'name'
? };
? let O3 = {
? ? id: 2,
? ? name: 'name'
? };
? O1 = O3; // ok
? O1 = {
? ? id: 2,
? ? name: 'name'
? } as I2; // ok
在示例中鞠眉,我們?cè)诘?5 行和第 13 行把包含多余屬性的類型賦值給了變量 O1薯鼠,沒有提示類型錯(cuò)誤。
另外械蹋,我們還需要注意類兼容性特性:實(shí)際上出皇,在判斷兩個(gè)類是否兼容時(shí),我們可以完全忽略其構(gòu)造函數(shù)及靜態(tài)屬性和方法是否兼容哗戈,只需要比較類實(shí)例的屬性和方法是否兼容即可郊艘。如果兩個(gè)類包含私有、受保護(hù)的屬性和方法,則僅當(dāng)這些屬性和方法源自同一個(gè)類纱注,它們才兼容畏浆。
下面我們看一個(gè)具體的示例:
{
? class C1 {
? ? name = '1';
? ? private id = 1;
? ? protected age = 30;
? }
? class C2 {
? ? name = '2';
? ? private id = 1;
? ? protected age = 30;
? }
? let InstC1: C1;
? let InstC2: C2;
? InstC1 = InstC2; // ts(2322)
? InstC2 = InstC1; // ts(2322)
}
{
? class CPar {
? ? private id = 1;
? ? protected age = 30;
? }
? class C1 extends CPar {
? ? constructor(inital: string) {
? ? ? super();
? ? }
? ? name = '1';
? ? static gender = 'man';
? }
? class C2 extends CPar {
? ? constructor(inital: number) {
? ? ? super();
? ? }
? ? name = '2';
? ? static gender = 'woman';
? }
? let InstC1: C1;
? let InstC2: C2;
? InstC1 = InstC2; // ok
? InstC2 = InstC1; // ok
}
在示例中的第 14~15 行,因?yàn)轭?C1 和類 C2 各自包含私有和受保護(hù)的屬性狞贱,且實(shí)例 InstC1 和 InstC2 不能相互賦值刻获,所以提示了一個(gè) ts(2322) 類型的錯(cuò)誤。
在第 38~39 行斥滤,因?yàn)轭?C1将鸵、類 C2 的私有、受保護(hù)屬性都繼承自同一個(gè)父類 CPar佑颇,所以檢測類型兼容性時(shí)會(huì)忽略其類型不相同的構(gòu)造函數(shù)和靜態(tài)屬性 gender顶掉,也因此實(shí)例 InstC1 和 實(shí)例 InstC2 之間可以相互賦值。
(3)可繼承和可實(shí)現(xiàn)
類型兼容性還決定了接口類型和類是否可以通過 extends 繼承另外一個(gè)接口類型或者類挑胸,以及類是否可以通過 implements 實(shí)現(xiàn)接口痒筒。
下面我們看一個(gè)具體示例:
{
? interface I1 {
? ? name: number;
? }
? interface I2 extends I1 { // ts(2430)
? ? name: string;
? }
? class C1 {
? ? name = '1';
? ? private id = 1;
? }
? class C2 extends C1 { // ts(2415)
? ? name = '2';
? ? private id = 1;
? }
? class C3 implements I1 {
? ? name = ''; // ts(2416)
? }
}
在示例中的第 5 行,因?yàn)榻涌陬愋?I1 和接口類型 I2 包含不同類型的 name 屬性不兼容茬贵,所以接口類型 I2 不能繼承接口類型 I1簿透。
同樣,在第 12 行解藻,因?yàn)轭?C1 和類 C2 不滿足類兼容條件老充,所以類 C2 也不能繼承類 C1。
而在第 16 行螟左,因?yàn)榻涌陬愋?I1 和類 C3 包含不同類型的 name 屬性啡浊,所以類 C3 不能實(shí)現(xiàn)接口類型 I1。
學(xué)習(xí)了類型兼容性的一般原則胶背,下面再來看看擁有類型入?yún)⒌姆盒汀?/p>
泛型
泛型類型巷嚣、泛型類的兼容性實(shí)際指的是將它們實(shí)例化為一個(gè)確切的類型后的兼容性。
可以通過指定類型入?yún)?shí)例化泛型钳吟,且入?yún)⒅挥凶鳛閷?shí)例化后的類型的一部分時(shí)才能影響類型兼容性廷粒,下面看一個(gè)具體的示例:
{
? interface I1<T> {
? ? id: number;
? }
? let O1: I1<string>;
? let O2: I1<number>;
? O1 = O2; // ol
}
在示例中的第 7 行,因?yàn)榻涌诜盒?I1 的入?yún)?T 是無用的红且,且實(shí)例化類型 I1<string> 和 I1<numer> 的結(jié)構(gòu)一致坝茎,即類型兼容,所以對(duì)應(yīng)的變量 O2 可以給變量 O1賦值暇番。
而對(duì)于未明確指定類型入?yún)⒎盒偷募嫒菪跃岸绾瘮?shù)泛型(實(shí)際上僅有函數(shù)泛型才可以在不需要實(shí)例化泛型的情況下賦值),TypeScript 會(huì)把 any 類型作為所有未明確指定的入?yún)㈩愋蛯?shí)例化泛型奔誓,然后再檢測其兼容性斤吐,如下代碼所示:
{
? let fun1 = <T>(p1: T): 1 => 1;
? let fun2 = <T>(p2: T): number => 2;
? fun2 = fun1; // ok搔涝?
}
在示例中的第 4 行,實(shí)際上相當(dāng)于在比較函數(shù)類型 (p1: any) => 1 和函數(shù)類型 (param: any) => number 的兼容性和措,那么這兩個(gè)函數(shù)的類型兼容嗎庄呈?答案:兼容。
為什么兼容呢派阱?這就涉及接下來我們要介紹的函數(shù)類型兼容性诬留。在此之前,我們先了解一下判定函數(shù)類型兼容性的基礎(chǔ)理論知識(shí):變型贫母。
變型
TypeScript 中的變型指的是根據(jù)類型之間的子類型關(guān)系推斷基于它們構(gòu)造的更復(fù)雜類型之間的子類型關(guān)系文兑。比如根據(jù) Dog 類型是 Animal 類型子類型這樣的關(guān)系,我們可以推斷數(shù)組類型 Dog[] 和 Animal[] 腺劣、函數(shù)類型 () => Dog 和 () => Animal 之間的子類型關(guān)系绿贞。
在描述類型和基于類型構(gòu)造的復(fù)雜類型之間的關(guān)系時(shí),我們可以使用數(shù)學(xué)中函數(shù)的表達(dá)方式橘原。比如 Dog 類型籍铁,我們可以使用 F(Dog) 表示構(gòu)造的復(fù)雜類型;F(Animal) 表示基于 Animal 構(gòu)造的復(fù)雜類型趾断。
這里的變型描述的就是基于 Dog 和 Animal 之間的子類型關(guān)系拒名,從而得出 F(Dog) 和 F(Animal) 之間的子類型關(guān)系的一般性質(zhì)。而這個(gè)性質(zhì)體現(xiàn)為子類型關(guān)系可能會(huì)被保持芋酌、反轉(zhuǎn)增显、忽略,因此它可以被劃分為協(xié)變脐帝、逆變同云、雙向協(xié)變和不變這 4 個(gè)專業(yè)術(shù)語。
接下來我們分別看一下這 4 個(gè)專業(yè)術(shù)語的具體定義腮恩。
(1)協(xié)變
協(xié)變也就是說如果 Dog 是 Animal 的子類型,則 F(Dog) 是 F(Animal) 的子類型温兼,這意味著在構(gòu)造的復(fù)雜類型中保持了一致的子類型關(guān)系秸滴,下面舉個(gè)簡單的例子:
{
? type isChild<Child, Par> = Child extends Par ? true : false;
? interface Animal {
? ? name: string;
? }
? interface Dog extends Animal {
? ? woof: () => void;
? }
? type Covariance<T> = T;
? type isCovariant = isChild<Covariance<Dog>, Covariance<Animal>>; // true
}
在示例中的第 1 行,我們首先定義了一個(gè)用來判斷兩個(gè)類型入?yún)?Child 和 Par 子類型關(guān)系的工具類型 isChild募判,如果 Child 是 Par 的子類型荡含,那么 isChild 會(huì)返回布爾字面量類型 true,否則返回 false届垫。
然后在第 3~8 行释液,我們定義了 Animal 類型和它的子類型 Dog。
在第 9 行装处,我們定義了泛型 Covariant 是一個(gè)復(fù)雜類型構(gòu)造器误债,因?yàn)樗獠粍?dòng)返回了類型入?yún)?T浸船,所以對(duì)于構(gòu)造出來的復(fù)雜類型 Covariant<Dog> 和 Covariant<Animal> 應(yīng)該與類型入?yún)?Dog 和 Animal 保持一致的子類型關(guān)系。
在第 10 行寝蹈,因?yàn)?Covariant<Dog> 是 Covariant<Animal> 的子類型李命,所以類型 isCovariant 是 true,這就是協(xié)變箫老。
實(shí)際上接口類型的屬性封字、數(shù)組類型、函數(shù)返回值的類型都是協(xié)變的耍鬓,下面看一個(gè)具體的示例:
? type isPropAssignmentCovariant = isChild<{ type: Dog }, { type: Animal }>; // true
? type isArrayElementCovariant = isChild<Dog[], Animal[]>; // true
? type isReturnTypeCovariant? = isChild<() => Dog, () => Animal>; // true
在示例中的第1~3 行阔籽,我們看到 isPropAssignmentCovariant、isArrayElementCovariant牲蜀、isReturnTypeCovariant 類型都是 true笆制,即接口類型 { type: Dog } 是 { type: Animal } 的子類型,數(shù)組類型 Dog[] 是 Animal[] 的子類型各薇,函數(shù)類型 () => Dog 也是 () => Animal 的子類型项贺。
(2)逆變
逆變也就是說如果 Dog 是 Animal 的子類型,則 F(Dog) 是 F(Animal) 的父類型峭判,這與協(xié)變正好反過來开缎。
實(shí)際場景中,在我們推崇的 TypeScript 嚴(yán)格模式下林螃,函數(shù)參數(shù)類型是逆變的奕删,具體示例如下:
? type Contravariance<T> = (param: T) => void;
? type isNotContravariance = isChild<Contravariance<Dog>, Contravariance<Animal>>; // false;
? type isContravariance = isChild<Contravariance<Animal>, Contravariance<Dog>>; // true;
在示例中的第 1 行,我們定義了一個(gè)基于類型入?yún)?gòu)造函數(shù)類型的構(gòu)造器 Contravariance疗认,且類型入?yún)?T 僅約束返回的函數(shù)類型參數(shù) param 的類型完残。因?yàn)?TypeScript 嚴(yán)格模式的設(shè)定是函數(shù)參數(shù)類型是逆變的,所以 Contravariance<Animal> 會(huì)是 Contravariance<Dog> 的子類型横漏,也因此第 2 行 isNotContravariance 是 false谨设,第 3 行 isContravariance 是 true。
為了更易于理解缎浇,我們可以從安全性的角度理解函數(shù)參數(shù)是逆變的設(shè)定扎拣。
如果函數(shù)參數(shù)類型是協(xié)變而不是逆變,那么意味著函數(shù)類型 (param: Dog) => void 和 (param: Animal) => void 是兼容的素跺,這與 Dog 和 Animal 的兼容一致二蓝,所以我們可以用 (param: Dog) => void 代替 (param: Animal) => void 遍歷 Animal[] 類型數(shù)組。
但是指厌,這樣是不安全的刊愚,因?yàn)樗荒艽_保 Animal[] 數(shù)組中的成員都是 Dog(可能混入 Animal 類型的其他子類型,比如 Cat)踩验,這就會(huì)導(dǎo)致 (param: Dog) => void 類型的函數(shù)可能接收到 Cat 類型的入?yún)ⅰ?/p>
下面我們來看一個(gè)具體示例:
? const visitDog = (animal: Dog) => {
? ? animal.woof();
? };
? let animals: Animal[] = [{ name: 'Cat', miao: () => void 0, }];
? animals.forEach(visitDog); // ts(2345)
在示例中鸥诽,如果函數(shù)參數(shù)類型是協(xié)變的商玫,那么第 5 行就可以通過靜態(tài)類型檢測,而不會(huì)提示一個(gè) ts(2345) 類型的錯(cuò)誤衙传。這樣第 1 行定義的 visitDog 函數(shù)在運(yùn)行時(shí)就能接收到 Dog 類型之外的入?yún)⒕鎏⒄{(diào)用不存在的 woof 方法,從而在運(yùn)行時(shí)拋出錯(cuò)誤蓖捶。
正是因?yàn)楹瘮?shù)參數(shù)是逆變的地回,所以使用 visitDog 函數(shù)遍歷 Animal[] 類型數(shù)組時(shí),在第 5 行提示了類型錯(cuò)誤俊鱼,因此也就不出現(xiàn) visitDog 接收到一只 cat 的情況刻像。
(3)雙向協(xié)變
雙向協(xié)變也就是說如果 Dog 是 Animal 的子類型,則 F(Dog) 是 F(Animal) 的子類型并闲,也是父類型细睡,既是協(xié)變也是逆變。
對(duì)應(yīng)到實(shí)際的場景帝火,在 TypeScript 非嚴(yán)格模式下溜徙,函數(shù)參數(shù)類型就是雙向協(xié)變的。如前邊提到函數(shù)只有在參數(shù)是逆變的情況下才安全犀填,且本課程一直在強(qiáng)調(diào)使用嚴(yán)格模式趁桃,所以雙向協(xié)變并不是一個(gè)安全或者有用的特性牵舱,因此我們不大可能遇到這樣的實(shí)際場景蛙埂。
但在某些資料中有提到室琢,如果函數(shù)參數(shù)類型是雙向協(xié)變,那么它是有用的冕广,并進(jìn)行了舉例論證 (以下示例縮減自網(wǎng)絡(luò)):
? interface Event {
? ? timestamp: number;
? }
? interface MouseEvent extends Event {
? ? x: number;
? ? y: number;
? }
? function addEventListener(handler: (n: Event) => void) {}
? addEventListener((e: MouseEvent) => console.log(e.x + ',' + e.y)); // ts(2769)
在示例中疏日,我們?cè)诘?4 行定義了接口 MouseEvent 是第 1 行定義的接口 Event 的子類型,在第 8 行定義了函數(shù)的 handler 參數(shù)是函數(shù)類型撒汉。如果參數(shù)類型是雙向協(xié)變的沟优,那么我們就可以在第 9 行把參數(shù)類型是 Event 子類型(比如說 MouseEvent 的函數(shù))作為入?yún)鹘o addEventListener。
這種方式確實(shí)方便了很多睬辐,但是并不安全挠阁,原因見前邊 Dog 和 Cat 的示例。而且在嚴(yán)格模式下溉委,參數(shù)類型是逆變而不是雙向協(xié)變的鹃唯,所以第 9 行提示了一個(gè) ts(2769) 的錯(cuò)誤爱榕。
由此可以得出瓣喊,真正有用且安全的做法是使用泛型,如下所示:
? function addEventListener<E extends Event>(handler: (n: E) => void) {}
? addEventListener((e: MouseEvent) => console.log(e.x + ',' + e.y)); // ok
在示例中的第 1 行黔酥,因?yàn)槲覀冎匦露x了帶約束條件泛型入?yún)⒌?addEventListener藻三,它可以傳遞任何參數(shù)類型是 Event 子類型的函數(shù)作為入?yún)⒑殚伲栽诘?2 行傳入?yún)?shù)類型是 MouseEvent 的箭頭函數(shù)作為入?yún)r(shí),則不會(huì)提示類型錯(cuò)誤棵帽。
(4)不變
不變即只要是不完全一樣的類型熄求,它們一定是不兼容的。也就是說即便 Dog 是 Animal 的子類型逗概,如果 F(Dog) 不是 F(Animal) 的子類型弟晚,那么 F(Animal) 也不是 F(Dog) 的子類型。
對(duì)應(yīng)到實(shí)際場景逾苫,出于類型安全層面的考慮卿城,在特定情況下我們可能希望數(shù)組是不變的(實(shí)際上是協(xié)變),見示例:
? interface Cat extends Animal {
? ? miao: () => void;
? }
? const cat: Cat = {
? ? name: 'Cat',
? ? miao: () => void 0,
? };
? const dog: Dog = {
? ? name: 'Dog',
? ? woof: () => void 0,
? };
? let dogs: Dog[] = [dog];
? animals = dogs; // ok
? animals.push(cat); // ok
? dogs.forEach(visitDog); // 類型 ok铅搓,但運(yùn)行時(shí)會(huì)拋出錯(cuò)誤
在示例中的第 1~3 行瑟押,我們定義了一個(gè) Animal 的另外一個(gè)子類 Cat。在第 4~8 行星掰,我們分別定義了對(duì)象 cat 和對(duì)象 dog多望,并在第 12 行定義了 Dog[] 類型的數(shù)組 dogs。
因?yàn)閿?shù)組是協(xié)變的氢烘,所以我們可以在第 13 行把 dogs 數(shù)組賦值給 animals 數(shù)組怀偷,并且在第 14 行把 cat 對(duì)象塞到 animals 數(shù)組中。那么問題就來了威始,因?yàn)?animals 和 dogs 指向的是同一個(gè)數(shù)組枢纠,所以實(shí)際上我們是把 cat 塞到了 dogs 數(shù)組中。
然后黎棠,我們?cè)诘?15 行使用了 visitDog 函數(shù)遍歷 dogs 數(shù)組晋渺。雖然它可以通過靜態(tài)類型檢測,但是運(yùn)行時(shí) visitDog 遍歷數(shù)組將接收一個(gè)混入的 cat 對(duì)象并拋出錯(cuò)誤脓斩,因?yàn)?visitDog 中調(diào)用了 cat 上沒有 woof 的方法木西。
因此,對(duì)于可變的數(shù)組而言随静,不變似乎是更安全八千、合理的設(shè)定。不過燎猛,在 TypeScript 中可變恋捆、不變的數(shù)組都是協(xié)變的,這是需要我們注意的一個(gè)陷阱重绷。
介紹完變型相關(guān)的術(shù)語以及對(duì)應(yīng)的實(shí)際場景沸停,我們已經(jīng)了解了函數(shù)參數(shù)類型是逆變的,返回值類型是協(xié)變的昭卓,所以前面的函數(shù)類型 (p1: any) => 1 和 (param: any) => number 為什么兼容的問題已經(jīng)給出答案了愤钾。因?yàn)榉祷刂殿愋?1 是 number 的子類型瘟滨,且返回值類型是協(xié)變的,所以 (p1: any) => 1 是 (param: any) => number 的子類型能颁,即是兼容的杂瘸。
函數(shù)類型兼容性
因?yàn)楹瘮?shù)類型的兼容性、子類型關(guān)系有著更復(fù)雜的考量(它還需要結(jié)合參數(shù)和返回值的類型進(jìn)行確定)伙菊,所以下面我們?cè)敿?xì)介紹一下函數(shù)類型兼容性的一般規(guī)則败玉。
(1)返回值
前邊我們已經(jīng)講過返回值類型是協(xié)變的,所以在參數(shù)類型兼容的情況下镜硕,函數(shù)的子類型關(guān)系與返回值子類型關(guān)系一致绒怨。也就是說返回值類型兼容,則函數(shù)兼容谦疾。
(2)參數(shù)類型
前邊我們也講過參數(shù)類型是逆變的南蹂,所以在參數(shù)個(gè)數(shù)相同、返回值類型兼容的情況下念恍,函數(shù)子類型關(guān)系與參數(shù)子類型關(guān)系是反過來的(逆變)六剥。
(3)參數(shù)個(gè)數(shù)
在索引位置相同的參數(shù)和返回值類型兼容的前提下,函數(shù)兼容性取決于參數(shù)個(gè)數(shù)峰伙,參數(shù)個(gè)數(shù)少的兼容個(gè)數(shù)多疗疟,下面我們看一個(gè)具體的示例:
{
? let lessParams = (one: number) => void 0;
? let moreParams = (one: number, two: string) => void 0;
? lessParams = moreParams; // ts(2322)
? moreParams = lessParams; // ok
}
在示例中,lessParams 參數(shù)個(gè)數(shù)少于 moreParams瞳氓,所以如第 5 行所示 lessParams 和 moreParams 兼容策彤,并可以賦值給 moreParams。
注意:如果你覺得參數(shù)個(gè)數(shù)少的函數(shù)兼容參數(shù)個(gè)數(shù)多的函數(shù)不好理解匣摘,那么可以試著從安全性角度理解(是參數(shù)少的函數(shù)賦值給參數(shù)多的函數(shù)安全店诗,還是參數(shù)多的函數(shù)賦值給參數(shù)少的函數(shù)安全),這里限于篇幅有限就不展開了(你可以作為思考題)音榜。
(4)可選和剩余參數(shù)
可選參數(shù)可以兼容剩余參數(shù)庞瘸、不可選參數(shù),下面我們具體看一個(gè)示例:
? let optionalParams = (one?: number, tow?: number) => void 0;
? let requiredParams = (one: number, tow: number) => void 0;
? let restParams = (...args: number[]) => void 0;
? requiredParams = optionalParams; // ok
? restParams = optionalParams; // ok
? optionalParams = restParams; // ts(2322)
? optionalParams = requiredParams; // ts(2322)
? restParams = requiredParams; // ok
? requiredParams = restParams; // ok
在示例中的第 4~5 行赠叼,我們可以把可選參數(shù) optionalParams 賦值給不可選參數(shù) requiredParams擦囊、剩余參數(shù) restParams ,反過來則提示了一個(gè) ts(2322) 的錯(cuò)誤(第 5~6 行)嘴办。
在第 8~9 行瞬场,不可選參數(shù) requiredParams 和剩余參數(shù) restParams 是互相兼容的;從安全性的角度理解第 9 行是安全的涧郊,所以可以賦值贯被。
最讓人費(fèi)解的是,在第 8 行中,把不可選參數(shù) requiredParams 賦值給剩余參數(shù) restParams 其實(shí)是不安全的(但是符合類型檢測)刃榨,我們需要從方便性上理解這個(gè)設(shè)定。
正是基于這個(gè)設(shè)定双仍,我們才可以將剩余參數(shù)類型函數(shù)定義為其他所有參數(shù)類型函數(shù)的父類型枢希,并用來約束其他類型函數(shù)的類型范圍,比如說在泛型中約束函數(shù)類型入?yún)⒌姆秶?/p>
下面我們看一個(gè)具體的示例:
type GetFun<F extends (...args: number[]) => any> = Parameters<F>;
type GetRequiredParams = GetFun<typeof requiredParams>;
type GetRestParams = GetFun<typeof restParams>;
type GetEmptyParams = GetFun<() => void>;
在示例中的第 1 行朱沃,我們使用剩余參數(shù)函數(shù)類型 (...args: number[]) => any 約束了入?yún)?F 的類型苞轿,而第 2~4 行傳入的函數(shù)類型入?yún)⒍际沁@個(gè)剩余參數(shù)函數(shù)類型的子類型。