定义
类型发生变化,变型都是发生在父子类型之间的。
结构化
Typescript是结构性的类型系统(Structural typing),就是对值所具有的结构进行类型检查的。因此,判断两个类型是否可以兼容,只要看两个类型的结构是否兼容就可以,不需要关心类型的名称是否相同。
这也是Typescript的一个特点,在其它一些静态语言(如c#、java)这样赋值是会报错的,因为严格意义上不是同一个类型。所以在这个特点下,具有结构兼容性的两个类型我们可以认为是父子类型关系。
上面举的例子是interface的,那如果是联合类型呢?
从interface类型来看,属性多的属于子类型,而在联合类型则是成员多的属于父类。其实无论是什么类型,父类型所描述的类型范围是更加广泛的(父类属性少限制就少),而子类型描述的类型是更加狭窄的,从父类到子类其实是一个narrowing过程。
协变(Covariance)
在TS,两个类型结构化相似的类型,把子类型赋值给父类型依然是安全的这种称为协变。(可参考类型兼容性的例子)
可以这样简单的理解,鸭子类型 —— “看起来像只鸭子,那就是只鸭子”
逆变(Contravariance)
大多数的类型兼容都是协变,但是涉及到函数时会不一样。
interface Animal {
age: number;
}
interface Dog extends Animal {
bark(): void;
}
let animal: Animal;
let dog: Dog;
let visitAnimal = (animal: Animal) => {
animal.age;
}
let visitDog = (dog: Dog) => {
dog.age;
}
Dog属于是Animal的子类型,animal=dog赋值是安全的,那visitAnimal=visitDog呢?
是不可行的!假如把visitAnimal赋值为visitDog,那调用visitAnimal传入的参数类型可能是没有bark的Animal类型,运行时会报错,类型是不安全的;
但是visitDog=visitAnimal是可行的,因为调用visitDog时接收到的参数永远是有age的;
父子关系逆转了,这种就属于逆变。
双向协变
在Typescript中,由于灵活性等权衡,函数参数默认是双向协变的。就是既可以visitDog=visitAnimal也可以visitAnimal=visitDog。这个可以在tsconfig中设置strictFunctionType或者strict让函数参数以逆变校验。
默认开启双向协变的原因:
不变
不变就很好理解了,就是不允许变型。两个不能兼容,类型结构不一样的类型不能变型。
对infer的影响
interface Foo {
(name: { a: string }): string;
(name: { b: number }): number;
}
type BAR<T> = T extends {
(name: infer R): infer Y;
(name: infer R): infer Y;
}
? [R, Y]
: never;
type BBB = BAR<Foo>;
简单来说,infer处于逆变位置推断出来的是交叉类型,处于协变位置推断出来的是联合类型。
使用例子
/**
* 联合类型 => 交叉类型
*/
type UnionToIntersection<U> = (U extends any ? (arg: U) => void : never) extends (arg: infer P) => void ? P : never;
总结
不管是协变还是逆变,归根到底都是在保证类型安全的前提下,提供一些灵活性。使用Typescript时,类型安全前提下,可以接受与定义不一样的类型,也不用刻意去记住什么是协变、逆变,记住一些特性,遇到问题时有思路可以用即可,毕竟typescript是一个工具。
参考:
www.typescriptlang.org/docs/handbo…
github.com/sl1673495/b…
www.jianshu.com/p/38bf7fedc…