我们为不同的变量定义了类型,所以不能像js一样,为一个变量自由赋值。类型兼容性就是描述一个类型能否被赋值为其他类型。兼容的本质是保证变量被赋值为其他类型的值,该变量在后续过程中仍能正常使用。
对于基础类型,他们的兼容性十分简单,在基础类型中也有提到,就不再赘述:
let a: number = 1;
a = "str"; // '"str"' is not assignable to type 'number'
但是ts还有很多更为复杂的类型,本节主要讲讲各种类型的兼容性。
结构兼容性
ts的类型兼容都是基于结构兼容,即比较的是是两个类型各自包含的属性,两个类型的名字不是重点。基本的原则是:如果Y
类型至少具有X
类型中的所有属性(X ⊆ Y
),那么X
类型兼容Y
类型。即X
=Y
合法:
interface X {
name: string;
}
interface Y {
name: string;
age: number;
}
let a: X;
let b: Y;
a = b; // 合法
b = a; // 不合法 Property 'age' is missing in type 'X' but required in type 'Y'.
Y
具有X
中的所有属性,所以X
兼容Y
,即a=b
合法。而X
却没有Y
所需要age
属性,所以Y
不兼容X
,即b=a
不合法。
大白话的解释就是:如果有一个类型为X
的变量a
,现在我要a
赋值,为了保证a
在赋值后,能正常调用它的属性和方法。那赋的值所具有的属性和方法一定是大于或等于a
所需要的(饱和式赋值)。如果a
需要name(string)
和age(number)
,给它赋的值只是name(string)
,当a
使用age
属性时就找不到。但是如果赋的值是name(string)
,age(number)
和other(any)
,那么没问题,a
所需要的都有,至于多余的属性,不用即可。
函数兼容性
函数兼容性相对来说要复杂一些,需要考虑两个方面:参数和返回值。先抛出结论:函数参数是逆变,返回值是协变(strictFunctionTypes
开启的前提下)。更多内容请查看函数的逆变协变。
参数兼容性
在返回值兼容的前提下,如果函数A
都每一个参数都能函数B
中找到对应的类型(关注的是类型兼容,参数名字不相同也没关系),即B ⊆ A
,那么函数A
兼容函数B
:
let fn1: (x: string) => void;
let fn2: (x: string, y: string) => void;
fn1 = fn2; // 不合法
fn2 = fn1; // 合法
咋一看好像和前面提到的结构兼容是冲突的,让我们结合具体的函数声明和实现再来看看:
let fn1: (x: string) => void = (x) => console.log(x);
let fn2: (x: string, y: string) => void = (x, y) => console.log(x, y);
fn2 = fn1;
fn2("x", "y"); // 多余的y不会被使用
fn1 = fn2;
fn1("x"); // fn2中的y拿不到需要的值
我们在比较两个函数是否兼容时,比较的是函数声明。函数声明规定了我们怎样传参才是合法的,但函数实现才实际消费这些参数。所以函数声明的参数需要大于等于实现中实际所需要的参数(饱和式传参)即实际目标函数参数 ⊆ 源函数声明参数
,才能保证函数实现能正常运行。
再次强调:我们为一个函数类型变量A
赋值另一个函数类型实现B
时,在调用该函数时,还是需要按照该函数A
声明传参,参数实际是被B
使用。所以函数参数的兼容实际上是参数传递和参数使用之间的兼容。
比较常见的例子就是高阶函数forEach
:
// array forEach的函数声明大概如下:
function forEach<T>(
callbackfn: (value: T, index: number, array: T[]) => void
): void;
const items = [1, 2, 3];
// 以下形式皆可
items.forEach((item, index, array) => console.log(item));
items.forEach(item => console.log(item));
forEach
在实际调用时,callbackFn
永远会被传递value
,index
,number
,array
四个变量,而我们传递callback
不一定都需要使用这些变量,可以只用到一部分。
对于可选参数,和剩余参数,在之前的基础上,多了以下规则:
- 源函数中多余的可选参数,不算错误
- 目标函数中多余的可选参数,即使没有和源函数中参数对应,不算错误
- 如果函数具有剩余参数,该参数会被认为无限个的可选类型参数(但实际上类型不会被认为是
undefined
和当前类型的联合类型,这是和真正的可选类型不同的地方)
let foo = (x: number, y: number) => {};
let bar = (x: number, y: number, z?: number) => {};
foo = bar; //合法,第2条规则
let foo = (x: number, y: number) => {};
let bar = (x?: number, y?: number) => {};
let baz = (...args: number[]) => {};
// 在strictNullChecks关闭时,以下都是合法的
foo = bar = baz;
baz = bar = foo;
// 否则
// 原因:可选类型的参数可能是 undefined,不能与 number 兼容
foo = baz;
foo = bar;
baz = foo;
baz = bar;
返回值兼容性
在函数参数兼容的前提下,如果源函数的返回值结构兼容目标函数的返回值(饱和式返回值),即源声明函数返回值 ⊆ 目标函数返回值
则源函数兼容目标函数:
let returnFn1: () => { name: string };
let returnFn2: () => { name: string; age: number };
returnFn1 = returnFn2; // 合法
returnFn2 = returnFn1; // 不合法
根据函数声明,当我们调用returnFn1
时返回值类型是{ name: string }
。但是调用returnFn1
时,实际执行的的是returnFn2
的函数实现。为了保证返回值能够正常使用name
属性。那么returnFn2
的返回值一定要包括name
,这就是第一节提到的结构兼容。
函数重载
对于重载函数,源函数每个重载的函数都需要和目标函数的重载兼容,即源函数重载 ⊆ 目标函数重载
。保证源函数的各个重载都能正常使用:
interface CallbackFn1 {
(a: number): number;
(a: string): string;
(a: string | number): string | number;
}
interface CallbackFn2 {
(a: string): string;
(a: number): string;
(a: string | number): string | number;
}
interface CallbackFn3 {
(a: string): string;
(a: number): number;
(a: number): string;
(a: string | number): string | number;
}
let cb1: CallbackFn1;
let cb2: CallbackFn2;
let cb3: CallbackFn3;
// 不合法
cb1 = cb2;
// 合法
cb1 = cb3;
可以看出,重载函数的兼容就是第一节提到的结构兼容。
枚举兼容性
数值枚举与数值相互兼容:
enum Status {
Ready,
Waiting,
}
let s: Status = Status.Ready;
let num = 10;
s = num;
num = s;
字符串兼容字符串枚举,反之不成立:
enum Status {
Ready = "ready",
Waiting = "waiting",
}
let s: Status = Status.Ready;
let str = "str";
s = str; // 不合法
str = s; // 合法
来自不同的枚举的变量相互不兼容:
enum Status {
Ready,
Waiting,
}
enum Color {
Red,
Blue,
Yellow,
}
let s: Status = Status.Ready;
let c: Color = Color.Red;
s = c; // 不合法
c = s; // 不合法
类兼容性
类也是结构兼容。类的兼容性只关注属性和方法,不会关注构造方法和静态成员:
class Animal {
feet: number = 1;
static prop: string;
static getSomething() {}
constructor(name: string, numFeet: number) {}
}
class Size {
feet: number = 0;
constructor(numFeet: number) {}
}
let animal: Animal;
let size: Size;
animal = size; // 合法
size = animal; // 合法
如果一个类具有私有或者受保护的成员,则必须来自相同的类:
class Animal {
protected feet: number = 1;
}
class Cat extends Animal {}
class Size {
protected feet: number = 0;
}
let animal: Animal;
let cat: Cat;
let size: Size;
cat = animal; // 合法,因为Cat属性和Animal一致
animal = cat; // 合法,兼容子类
size = animal; // 不合法,feet不是来自同一个类
animal = size; // 不合法,同上
泛型兼容性
正如前面提到的ts的类型兼容是基于结构兼容。泛型的类型参数没有被使用时不会影响兼容性:
interface Empty<T> {
}
let x: Empty<number>;
let y: Empty<string>;
x = y; // 合法,结构兼容
当类型参数在被使用后才会影响到兼容性:
interface Empty<T> {
prop: T;
}
let x: Empty<number>;
let y: Empty<string>;
x = y; // 不合法 Type 'Empty<string>' is not assignable to type 'Empty<number>'.Type 'string' is not assignable to type 'number'
如果泛型还未被实例化,在检查兼容性时会被类型参数转换为any
:
let identity = function <T>(x: T): void {
};
let reverse = function <U>(y: U): void {
};
identity = reverse; // 合法,(x: any) => void兼容(x: any) => void