TypeScript類型兼容判斷

因?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ù)類型的子類型。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末逗物,一起剝皮案震驚了整個(gè)濱河市搬卒,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌翎卓,老刑警劉巖契邀,帶你破解...
    沈念sama閱讀 216,997評(píng)論 6 502
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異失暴,居然都是意外死亡坯门,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,603評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門逗扒,熙熙樓的掌柜王于貴愁眉苦臉地迎上來古戴,“玉大人,你說我怎么就攤上這事矩肩∠帜眨” “怎么了?”我有些...
    開封第一講書人閱讀 163,359評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵黍檩,是天一觀的道長叉袍。 經(jīng)常有香客問我,道長刽酱,這世上最難降的妖魔是什么畦韭? 我笑而不...
    開封第一講書人閱讀 58,309評(píng)論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮肛跌,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘衍慎。我一直安慰自己,他們只是感情好稳捆,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,346評(píng)論 6 390
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般款侵。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上侧纯,一...
    開封第一講書人閱讀 51,258評(píng)論 1 300
  • 那天,我揣著相機(jī)與錄音妹笆,去河邊找鬼。 笑死拳缠,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的窟坐。 我是一名探鬼主播,決...
    沈念sama閱讀 40,122評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼绵疲,長吁一口氣:“原來是場噩夢(mèng)啊……” “哼狸涌!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起最岗,我...
    開封第一講書人閱讀 38,970評(píng)論 0 275
  • 序言:老撾萬榮一對(duì)情侶失蹤帕胆,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后般渡,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體懒豹,經(jīng)...
    沈念sama閱讀 45,403評(píng)論 1 313
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,596評(píng)論 3 334
  • 正文 我和宋清朗相戀三年驯用,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了脸秽。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,769評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡蝴乔,死狀恐怖记餐,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情薇正,我是刑警寧澤片酝,帶...
    沈念sama閱讀 35,464評(píng)論 5 344
  • 正文 年R本政府宣布,位于F島的核電站挖腰,受9級(jí)特大地震影響雕沿,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜猴仑,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,075評(píng)論 3 327
  • 文/蒙蒙 一审轮、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦疾渣、人聲如沸篡诽。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,705評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽杈女。三九已至,卻和暖如春薄疚,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背赊琳。 一陣腳步聲響...
    開封第一講書人閱讀 32,848評(píng)論 1 269
  • 我被黑心中介騙來泰國打工街夭, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人板丽。 一個(gè)月前我還...
    沈念sama閱讀 47,831評(píng)論 2 370
  • 正文 我出身青樓埃碱,卻偏偏與公主長得像酥泞,于是被迫代替她去往敵國和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子似炎,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,678評(píng)論 2 354

推薦閱讀更多精彩內(nèi)容