前言
使用 Typescript 开发的同学一定遇到过这个报错:
Type A is not assignable to type B. -> 类型 A 不可分配给类型 B
而这个错误的原因,就是类型不兼容。
如果你常常对此类错误感到费解,不妨往下读一读。
类型兼容
了解类型兼容之前需要先了解另一个小概念-结构子类型(也称为鸭子辨型):
是一种只使用其成员来描述类型的方式。
对于TS类型系统而言:像鸭子一样走路、游泳和嘎嘎叫的鸟就是鸭子。
interface Duck {
walk: () => void;
swim: () => void;
gaga: () => void;
}
class WhateverType {
walk = () => { };
swim = () => { };
gaga = () => { };
}
let duck: Duck;
// 完全 OK。
// 类型系统检测到 WhateverType 有和类型 Duck 一样的成员,则认为 WhateverType 实例也可以作为 Duck 的实例
duck = new WhateverType();
TS的类型兼容性就是基于结构子类型的。
什么是类型兼容
概念
TypeScript结构化类型系统的基本规则是,如果 A 要兼容 B,那么 B 至少具有与 A 相同的属性。
概念有点抽象,来举个🌰:
我们先来看看集合中子集的概念。
如果集合A的任意一个元素都是集合B的元素(任意a∈A则a∈B),那么集合A称为集合B的子集 。
而 上述提到的 A 兼容 B 条件是: “ B 至少具有与 A 相同的属性”
这个条件翻译到集合中即:B ⊇ A ,也就是 A ⊆B。
所以这个概念用集合解释就是:如果 A ⊆B , 则 A 兼容 B。
类型分配
如果 A 兼容 B,则TS类型系统会视为类型 B 可以分配给类型 A。(反之亦然)
再举个🌰,来看如下代码示例:
interface Animal {
name: string;
}
interface Dog {
name: string;
wow: () => void;
}
let animal: Animal = {
name: ''
};
let dog: Dog = {
name: '',
wow: () => console.log('wow')
};
animal = dog; // ok
dog = animal; // error
因为TypeScript里的类型兼容性是基于结构子类型的,所以我们可以认为:
Animal ⊆ Dog => 则 Animal 兼容 Dog => 则 Dog 类型可以分配给 Animal 类型 => animal = dog(is ok)。
而 Dog ⊊ Animal , 同理可得 dog = animal (is error )。
二者的类型关系可用 extends 的来描述:Dog extends Animal 。(称 Dog 为 Animal 的子类型)
这也就是文章开头提到的常见报错中的一种。
一句话总结TS的类型分配:更具体的类型可以分配给更宽泛的类型。
PS:对于联合类型 type Parent = "a" | "b" ; type Child = "a"; child 比 parent 更具体。
有了这个理论基础,我们再来看TS是怎么处理高阶类型的兼容问题的。
高阶类型的兼容型
PS:TS官方没有高阶类型的概念。类比JS的高阶函数(函数本身的功能是 入参或返回值是函数 的函数),高阶类型即可接受一个泛型(也可以是确定的类型),并返回一个复合类型的 工具类型。如 Array就是一个高阶类型
TS类型系统在检测类型分配的时候,(常出现于 “=” 赋值、函数调用),会根据一些规则去判断它们的类型兼容性,而这个规则,就是接下来要讲的四个概念:协变、逆变、双向协变、不变 ?
假设我们将基础类型叫做T,复合类型叫做Complex,那么:
- 协变表示Complex类型兼容和T的一致。
- 逆变表示Complex类型兼容和T相反。
- 双向协变表示Complex类型与T类型双向兼容。
- 不变表示Complex类型与T类型双向都不兼容。
一下子引入了四个概念,莫慌,都比较好理解,下面一一举🌰说明:
协变 (Covariant)
以函数的返回值类型兼容性举例。
interface Animal {
name: string;
}
interface Dog {
name: string;
wow: () => void;
}
declare let animal: Animal;
declare let animalArray: Array<Animal>;
declare let dog: Dog;
declare let dogArray: Array<Dog>;
animal = dog; // ok
dog = animal; // error
animalArray = dogArray; // ok
dogArray = animalArray; // error
- Animal 兼容 Dog => Array 兼容 Array
可以看到 Array 的兼容性和 T 的兼容性是一致的。—— 这就是协变
通俗易懂版:狗是动物 => 几只狗是几只动物。
逆变 (Contravariant)
以方法的入参举例。
interface Animal {
name: string;
}
interface Dog {
name: string;
wow: () => void;
}
type Func<T> = (t: T) => void;
declare let animal: Animal;
declare let animalFunc: Func<Animal>;
declare let dog: Dog;
declare let dogFunc: Func<Dog>;
animal = dog; // ok
dog = animal; // error
animalFunc = dogFunc; // error
dogFunc = animalFunc; // ok
- Animal 兼容 Dog ,Dog 不兼容 Animal
- Func 不兼容 Func ,Func 兼容 Func
可以看到 Func 的兼容性和 T 的兼容性是刚好相反的。—— 这就是逆变
那么为什么这样设计呢?因为在 Func 中可能会访问到 Func 中没有的成员,而 Func 中不可能会访问到 Func 中没有的成员。
通俗易懂版:我需要一条狗用于看家(Func),希望它看到陌生人的时候可以汪汪汪(dog.wow() ),但是你随便给了我一只动物,我让它看家,它却不会汪汪汪。这是我不能接受的。
双向协变 (Bivariant)
理解了协变和逆变,再来看看后面两个就简单了。
双向协变是一个几乎不会用到的场景(在非严格模式下,函数的参数就是双向协变。因为会影响类型安全,所以在TS2.6版本以后,需要在tsConfig中手动开启 strictFunctionTypes )
interface Animal {
name: string;
}
interface Dog {
name: string;
wow: () => void;
}
type Func<T> = (t: T) => void;
declare let animal: Animal;
declare let animalFunc: Func<Animal>;
declare let dog: Dog;
declare let dogFunc: Func<Dog>;
animal = dog; // ok
dog = animal; // error
animalFunc = dogFunc; // ok
dogFunc = animalFunc; // ok
Animal 兼容 dog => animalFunc 和 dogFunc 互相兼容。
非特殊情况,推荐使用默认的逆变规则。
不变 (Invariant)
这里需要构造一个复合类型。
interface Animal {
name: string;
}
interface Dog {
name: string;
wow: () => void;
}
interface Who<T> {
item: T;
func: (item: T) => void
}
declare let animal: Animal;
declare let animalWho: Who<Animal>;
declare let dog: Dog;
declare let dogWho: Who<Dog>;
animal = dog; // ok
dog = animal; // error
animalWho = dogWho; // error Type '(t: Dog) => void' is not assignable to type '(t: Animal) => void'
dogWho = animalWho; // error Type 'Animal' is not assignable to type 'Dog'
Wow 不兼容 Wow,这是由于方法入参是逆变。
Wow 不兼容 Wow,这是由于 Dog 不兼容 Animal。
像这样,二者都不兼容的情况,就称为不变(不可变)。
总结
TS的类型兼容型的设计是非常巧妙的,既保证了类型安全,又为TS语言提供了很大了灵活性。对于刚刚从JS转到TS的同学,理解类型兼容是非常有必要的,能让你迅速地在TS的类型安全和JS的灵活之间游刃有余。
PS:如有讲的不对的地方,欢迎指正!