因為 TypeScript 中有靜態(tài)類型檢測,所以我們再也不用像 JavaScript 中那樣,賦給變量任意類型的值。
在 TypeScript 中,能不能把一個類型賦值給其他類型是由類型兼容性決定的。
特例
首先,我們回顧一下 any、never、unknown 等特殊類型,它們在類型兼容性上十分有特色。
(1)any
萬金油 any 類型可以賦值給除了 never 之外的任意其他類型,反過來其他類型也可以賦值給 any。也就是說 any 可以兼容除 never 之外所有的類型,同時也可以被所有的類型兼容(即 any 既是 bottom type,也是 top type)。因為 any 太特殊,這里我就不舉例子了。
再次強調:Any is 魔鬼,我們一定要慎用、少用。
(2)never
never 的特性是可以賦值給任何其他類型,但反過來不能被其他任何類型(包括 any 在內)賦值(即 never 是 bottom type)。比如以下示例中的第 5~7 行,我們可以把 never 賦值給 number、函數、對象類型。
{
? 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 行提示了一個 ts(2322) unknown 類型不能賦值給其他任何類型的錯誤。
{
? 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
}
在我們推崇并使用的嚴格模式下,null、undefined 表現(xiàn)出與 void 類似的兼容性,即不能賦值給除 any 和 unknown 之外的其他類型,反過來其他類型(除了 any 和 never 之外)都不可以賦值給 null 或 undefined。
(5)enum
最后一個特例是 enum 枚舉類型,其中數字枚舉和數字類型相互兼容。
在如下示例中,我們在第 5 行把枚舉 A 賦值給了數字(number)類型,并在第 7 行使用數字字面量 1 替代了枚舉 A。
{
? enum A {
? ? one
? }
? let num: number = A.one; // ok
? let fun = (param: A) => void 0;
? fun(1); // ok
}
此外,不同枚舉之間不兼容。如下示例中的第 10~11 行,因為枚舉 A 和 B 不兼容,所以都會提示一個 ts(2322) 類型的錯誤。
{
? enum A {
? ? one
? }
? enum B {
? ? one
? }
? let a: A;
? let b: B;
? a = b; // ts(2322)
? b = a; // ts(2322)
}
類型兼容性
除了前邊提到的所有特例,TypeScript 中類型的兼容性都是基于結構化子類型的一般原則進行判定的。
下面我們從結構化類型和子類型這兩方面了解一下一般原則。
(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 行,我們可以把類型是數字字面量類型的 one 賦值給數字類型的 num。在第 12 行,我們可以把子接口類型的變量賦值給 Par。在第 21 行,我們可以把子類實例 ChildInst 賦值給 ParInst。
因為成員類型兼容它所屬的類型集合(其實聯(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 行,因為 IChild 是 IPar 的子類,CChild 是 CPar 的子類,所以 IChild | CChild 也是 IPar | CPar 的子類,進而 ICChild 可以賦值給 ICPar。
(2)結構類型
類型兼容性的另一準則是結構類型,即如果兩個類型的結構一致,則它們是互相兼容的。比如擁有相同類型的屬性、方法的接口類型或類,則可以互相賦值。
下面我們看一個具體的示例:
{
? 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
}
因為類 C1、類 C2、接口類型 I1、接口類型 I2 的結構完全一致,所以在第 18~19 行我們可以把類 C2 的實例 InstC2 賦值給類 C1 的實例 Inst1,把接口類型 I2 的變量 O2 賦值給接口類型 I1 的變量 O1。
在第 20~21 行,我們甚至可以把接口類型 I1 的變量 O1 賦值給類 C1 的實例,類 C2 的實例賦值給接口類型 I2 的變量 O2。
另外一個特殊的場景:兩個接口類型或者類,如果其中一個類型不僅擁有另外一個類型全部的屬性和方法,還包含其他的屬性和方法(如同繼承自另外一個類型的子類一樣),那么前者是可以兼容后者的。
下面我們看一個具體的示例:
{
? 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 的實例 InstC2 和接口類型 I2 的變量 O2 賦值給接口類型 I1 的變量 O1,這是因為類 C2、接口類型 I2 和接口類型 I1 的 name 屬性都是 string。不過,因為變量 O2、類 C2 都包含了額外的屬性 id,所以我們不能把變量 O1 賦值給實例 InstC2、變量 O2。
這里涉及一個需要特別注意的特性:雖然包含多余屬性 id 的變量 O2 可以賦值給變量 O1,但是如果我們直接將一個與變量 O2 完全一樣結構的對象字面量賦值給變量 O1,則會提示一個 ts(2322) 類型不兼容的錯誤(如下示例第 2 行),這就是對象字面的 freshness 特性。
也就是說一個對象字面量沒有被變量接收時,它將處于一種 freshness 新鮮的狀態(tài)。這時 TypeScript 會對對象字面量的賦值操作進行嚴格的類型檢測,只有目標變量的類型與對象字面量的類型完全一致時,對象字面量才可以賦值給目標變量,否則會提示類型錯誤。
當然,我們也可以通過使用變量接收對象字面量或使用類型斷言解除 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
在示例中,我們在第 5 行和第 13 行把包含多余屬性的類型賦值給了變量 O1,沒有提示類型錯誤。
另外,我們還需要注意類兼容性特性:實際上,在判斷兩個類是否兼容時,我們可以完全忽略其構造函數及靜態(tài)屬性和方法是否兼容,只需要比較類實例的屬性和方法是否兼容即可。如果兩個類包含私有、受保護的屬性和方法,則僅當這些屬性和方法源自同一個類,它們才兼容。
下面我們看一個具體的示例:
{
? 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 行,因為類 C1 和類 C2 各自包含私有和受保護的屬性,且實例 InstC1 和 InstC2 不能相互賦值,所以提示了一個 ts(2322) 類型的錯誤。
在第 38~39 行,因為類 C1、類 C2 的私有、受保護屬性都繼承自同一個父類 CPar,所以檢測類型兼容性時會忽略其類型不相同的構造函數和靜態(tài)屬性 gender,也因此實例 InstC1 和 實例 InstC2 之間可以相互賦值。
(3)可繼承和可實現(xiàn)
類型兼容性還決定了接口類型和類是否可以通過 extends 繼承另外一個接口類型或者類,以及類是否可以通過 implements 實現(xiàn)接口。
下面我們看一個具體示例:
{
? 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 行,因為接口類型 I1 和接口類型 I2 包含不同類型的 name 屬性不兼容,所以接口類型 I2 不能繼承接口類型 I1。
同樣,在第 12 行,因為類 C1 和類 C2 不滿足類兼容條件,所以類 C2 也不能繼承類 C1。
而在第 16 行,因為接口類型 I1 和類 C3 包含不同類型的 name 屬性,所以類 C3 不能實現(xiàn)接口類型 I1。
學習了類型兼容性的一般原則,下面再來看看擁有類型入參的泛型。
泛型
泛型類型、泛型類的兼容性實際指的是將它們實例化為一個確切的類型后的兼容性。
可以通過指定類型入參實例化泛型,且入參只有作為實例化后的類型的一部分時才能影響類型兼容性,下面看一個具體的示例:
{
? interface I1<T> {
? ? id: number;
? }
? let O1: I1<string>;
? let O2: I1<number>;
? O1 = O2; // ol
}
在示例中的第 7 行,因為接口泛型 I1 的入參 T 是無用的,且實例化類型 I1<string> 和 I1<numer> 的結構一致,即類型兼容,所以對應的變量 O2 可以給變量 O1賦值。
而對于未明確指定類型入參泛型的兼容性,例如函數泛型(實際上僅有函數泛型才可以在不需要實例化泛型的情況下賦值),TypeScript 會把 any 類型作為所有未明確指定的入參類型實例化泛型,然后再檢測其兼容性,如下代碼所示:
{
? let fun1 = <T>(p1: T): 1 => 1;
? let fun2 = <T>(p2: T): number => 2;
? fun2 = fun1; // ok?
}
在示例中的第 4 行,實際上相當于在比較函數類型 (p1: any) => 1 和函數類型 (param: any) => number 的兼容性,那么這兩個函數的類型兼容嗎?答案:兼容。
為什么兼容呢?這就涉及接下來我們要介紹的函數類型兼容性。在此之前,我們先了解一下判定函數類型兼容性的基礎理論知識:變型。
變型
TypeScript 中的變型指的是根據類型之間的子類型關系推斷基于它們構造的更復雜類型之間的子類型關系。比如根據 Dog 類型是 Animal 類型子類型這樣的關系,我們可以推斷數組類型 Dog[] 和 Animal[] 、函數類型 () => Dog 和 () => Animal 之間的子類型關系。
在描述類型和基于類型構造的復雜類型之間的關系時,我們可以使用數學中函數的表達方式。比如 Dog 類型,我們可以使用 F(Dog) 表示構造的復雜類型;F(Animal) 表示基于 Animal 構造的復雜類型。
這里的變型描述的就是基于 Dog 和 Animal 之間的子類型關系,從而得出 F(Dog) 和 F(Animal) 之間的子類型關系的一般性質。而這個性質體現(xiàn)為子類型關系可能會被保持、反轉、忽略,因此它可以被劃分為協(xié)變、逆變、雙向協(xié)變和不變這 4 個專業(yè)術語。
接下來我們分別看一下這 4 個專業(yè)術語的具體定義。
(1)協(xié)變
協(xié)變也就是說如果 Dog 是 Animal 的子類型,則 F(Dog) 是 F(Animal) 的子類型,這意味著在構造的復雜類型中保持了一致的子類型關系,下面舉個簡單的例子:
{
? 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 行,我們首先定義了一個用來判斷兩個類型入參 Child 和 Par 子類型關系的工具類型 isChild,如果 Child 是 Par 的子類型,那么 isChild 會返回布爾字面量類型 true,否則返回 false。
然后在第 3~8 行,我們定義了 Animal 類型和它的子類型 Dog。
在第 9 行,我們定義了泛型 Covariant 是一個復雜類型構造器,因為它原封不動返回了類型入參 T,所以對于構造出來的復雜類型 Covariant<Dog> 和 Covariant<Animal> 應該與類型入參 Dog 和 Animal 保持一致的子類型關系。
在第 10 行,因為 Covariant<Dog> 是 Covariant<Animal> 的子類型,所以類型 isCovariant 是 true,這就是協(xié)變。
實際上接口類型的屬性、數組類型、函數返回值的類型都是協(xié)變的,下面看一個具體的示例:
? 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 } 的子類型,數組類型 Dog[] 是 Animal[] 的子類型,函數類型 () => Dog 也是 () => Animal 的子類型。
(2)逆變
逆變也就是說如果 Dog 是 Animal 的子類型,則 F(Dog) 是 F(Animal) 的父類型,這與協(xié)變正好反過來。
實際場景中,在我們推崇的 TypeScript 嚴格模式下,函數參數類型是逆變的,具體示例如下:
? type Contravariance<T> = (param: T) => void;
? type isNotContravariance = isChild<Contravariance<Dog>, Contravariance<Animal>>; // false;
? type isContravariance = isChild<Contravariance<Animal>, Contravariance<Dog>>; // true;
在示例中的第 1 行,我們定義了一個基于類型入參構造函數類型的構造器 Contravariance,且類型入參 T 僅約束返回的函數類型參數 param 的類型。因為 TypeScript 嚴格模式的設定是函數參數類型是逆變的,所以 Contravariance<Animal> 會是 Contravariance<Dog> 的子類型,也因此第 2 行 isNotContravariance 是 false,第 3 行 isContravariance 是 true。
為了更易于理解,我們可以從安全性的角度理解函數參數是逆變的設定。
如果函數參數類型是協(xié)變而不是逆變,那么意味著函數類型 (param: Dog) => void 和 (param: Animal) => void 是兼容的,這與 Dog 和 Animal 的兼容一致,所以我們可以用 (param: Dog) => void 代替 (param: Animal) => void 遍歷 Animal[] 類型數組。
但是,這樣是不安全的,因為它不能確保 Animal[] 數組中的成員都是 Dog(可能混入 Animal 類型的其他子類型,比如 Cat),這就會導致 (param: Dog) => void 類型的函數可能接收到 Cat 類型的入參。
下面我們來看一個具體示例:
? const visitDog = (animal: Dog) => {
? ? animal.woof();
? };
? let animals: Animal[] = [{ name: 'Cat', miao: () => void 0, }];
? animals.forEach(visitDog); // ts(2345)
在示例中,如果函數參數類型是協(xié)變的,那么第 5 行就可以通過靜態(tài)類型檢測,而不會提示一個 ts(2345) 類型的錯誤。這樣第 1 行定義的 visitDog 函數在運行時就能接收到 Dog 類型之外的入參,并調用不存在的 woof 方法,從而在運行時拋出錯誤。
正是因為函數參數是逆變的,所以使用 visitDog 函數遍歷 Animal[] 類型數組時,在第 5 行提示了類型錯誤,因此也就不出現(xiàn) visitDog 接收到一只 cat 的情況。
(3)雙向協(xié)變
雙向協(xié)變也就是說如果 Dog 是 Animal 的子類型,則 F(Dog) 是 F(Animal) 的子類型,也是父類型,既是協(xié)變也是逆變。
對應到實際的場景,在 TypeScript 非嚴格模式下,函數參數類型就是雙向協(xié)變的。如前邊提到函數只有在參數是逆變的情況下才安全,且本課程一直在強調使用嚴格模式,所以雙向協(xié)變并不是一個安全或者有用的特性,因此我們不大可能遇到這樣的實際場景。
但在某些資料中有提到,如果函數參數類型是雙向協(xié)變,那么它是有用的,并進行了舉例論證 (以下示例縮減自網絡):
? 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)
在示例中,我們在第 4 行定義了接口 MouseEvent 是第 1 行定義的接口 Event 的子類型,在第 8 行定義了函數的 handler 參數是函數類型。如果參數類型是雙向協(xié)變的,那么我們就可以在第 9 行把參數類型是 Event 子類型(比如說 MouseEvent 的函數)作為入參傳給 addEventListener。
這種方式確實方便了很多,但是并不安全,原因見前邊 Dog 和 Cat 的示例。而且在嚴格模式下,參數類型是逆變而不是雙向協(xié)變的,所以第 9 行提示了一個 ts(2769) 的錯誤。
由此可以得出,真正有用且安全的做法是使用泛型,如下所示:
? function addEventListener<E extends Event>(handler: (n: E) => void) {}
? addEventListener((e: MouseEvent) => console.log(e.x + ',' + e.y)); // ok
在示例中的第 1 行,因為我們重新定義了帶約束條件泛型入參的 addEventListener,它可以傳遞任何參數類型是 Event 子類型的函數作為入參,所以在第 2 行傳入參數類型是 MouseEvent 的箭頭函數作為入參時,則不會提示類型錯誤。
(4)不變
不變即只要是不完全一樣的類型,它們一定是不兼容的。也就是說即便 Dog 是 Animal 的子類型,如果 F(Dog) 不是 F(Animal) 的子類型,那么 F(Animal) 也不是 F(Dog) 的子類型。
對應到實際場景,出于類型安全層面的考慮,在特定情況下我們可能希望數組是不變的(實際上是協(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,但運行時會拋出錯誤
在示例中的第 1~3 行,我們定義了一個 Animal 的另外一個子類 Cat。在第 4~8 行,我們分別定義了對象 cat 和對象 dog,并在第 12 行定義了 Dog[] 類型的數組 dogs。
因為數組是協(xié)變的,所以我們可以在第 13 行把 dogs 數組賦值給 animals 數組,并且在第 14 行把 cat 對象塞到 animals 數組中。那么問題就來了,因為 animals 和 dogs 指向的是同一個數組,所以實際上我們是把 cat 塞到了 dogs 數組中。
然后,我們在第 15 行使用了 visitDog 函數遍歷 dogs 數組。雖然它可以通過靜態(tài)類型檢測,但是運行時 visitDog 遍歷數組將接收一個混入的 cat 對象并拋出錯誤,因為 visitDog 中調用了 cat 上沒有 woof 的方法。
因此,對于可變的數組而言,不變似乎是更安全、合理的設定。不過,在 TypeScript 中可變、不變的數組都是協(xié)變的,這是需要我們注意的一個陷阱。
介紹完變型相關的術語以及對應的實際場景,我們已經了解了函數參數類型是逆變的,返回值類型是協(xié)變的,所以前面的函數類型 (p1: any) => 1 和 (param: any) => number 為什么兼容的問題已經給出答案了。因為返回值類型 1 是 number 的子類型,且返回值類型是協(xié)變的,所以 (p1: any) => 1 是 (param: any) => number 的子類型,即是兼容的。
函數類型兼容性
因為函數類型的兼容性、子類型關系有著更復雜的考量(它還需要結合參數和返回值的類型進行確定),所以下面我們詳細介紹一下函數類型兼容性的一般規(guī)則。
(1)返回值
前邊我們已經講過返回值類型是協(xié)變的,所以在參數類型兼容的情況下,函數的子類型關系與返回值子類型關系一致。也就是說返回值類型兼容,則函數兼容。
(2)參數類型
前邊我們也講過參數類型是逆變的,所以在參數個數相同、返回值類型兼容的情況下,函數子類型關系與參數子類型關系是反過來的(逆變)。
(3)參數個數
在索引位置相同的參數和返回值類型兼容的前提下,函數兼容性取決于參數個數,參數個數少的兼容個數多,下面我們看一個具體的示例:
{
? let lessParams = (one: number) => void 0;
? let moreParams = (one: number, two: string) => void 0;
? lessParams = moreParams; // ts(2322)
? moreParams = lessParams; // ok
}
在示例中,lessParams 參數個數少于 moreParams,所以如第 5 行所示 lessParams 和 moreParams 兼容,并可以賦值給 moreParams。
注意:如果你覺得參數個數少的函數兼容參數個數多的函數不好理解,那么可以試著從安全性角度理解(是參數少的函數賦值給參數多的函數安全,還是參數多的函數賦值給參數少的函數安全),這里限于篇幅有限就不展開了(你可以作為思考題)。
(4)可選和剩余參數
可選參數可以兼容剩余參數、不可選參數,下面我們具體看一個示例:
? 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 行,我們可以把可選參數 optionalParams 賦值給不可選參數 requiredParams、剩余參數 restParams ,反過來則提示了一個 ts(2322) 的錯誤(第 5~6 行)。
在第 8~9 行,不可選參數 requiredParams 和剩余參數 restParams 是互相兼容的;從安全性的角度理解第 9 行是安全的,所以可以賦值。
最讓人費解的是,在第 8 行中,把不可選參數 requiredParams 賦值給剩余參數 restParams 其實是不安全的(但是符合類型檢測),我們需要從方便性上理解這個設定。
正是基于這個設定,我們才可以將剩余參數類型函數定義為其他所有參數類型函數的父類型,并用來約束其他類型函數的類型范圍,比如說在泛型中約束函數類型入參的范圍。
下面我們看一個具體的示例:
type GetFun<F extends (...args: number[]) => any> = Parameters<F>;
type GetRequiredParams = GetFun<typeof requiredParams>;
type GetRestParams = GetFun<typeof restParams>;
type GetEmptyParams = GetFun<() => void>;
在示例中的第 1 行,我們使用剩余參數函數類型 (...args: number[]) => any 約束了入參 F 的類型,而第 2~4 行傳入的函數類型入參都是這個剩余參數函數類型的子類型。