学过集合论的同窗必定知道子集的概念,使用ES6 class写过继承的同窗必定知道子类的概念,而使用过TypeScript的同窗,也许知道子类型的概念。git
可是你知道协变 (Covariant)、逆变 (Contravariant)、双向协变 (Bivariant) 和不变 (Invariant) 这些概念吗?你知道像TypeScript这种强大的静态类型检查的编程语言,是怎么作类型兼容的吗?咱们今天来聊聊。github
子类型是编程语言中一个有趣的概念,源自于数学中子集的概念:typescript
若是集合A的任意一个元素都是集合B的元素,那么集合A称为集合B的子集。编程
而子类型则是面向对象设计语言里常提到的一个概念,是继承机制的一个产物,如下概念来源百度:数组
在编程语言理论中,子类型是一种类型多态的形式。这种形式下,子类型能够替换另外一种相关的数据类型(超类型,英语:supertype)。安全
子类型与面向对象语言中(类或对象)的继承是两个概念。子类型反映了类型(即面向对象中的接口)之间的关系;而继承反映了一类对象能够从另外一类对象创造出来,是语言特性的实现。所以,子类型也称接口继承;继承称做实现继承。编程语言
咱们能够理解子类就是实现继承,子类型就是接口继承,下面这幅图更精确的定义了这个概念,不少同窗应该知道这个例子:ide
这幅图中,猫是一种动物,因此咱们说猫是动物的子集,猫是动物的子类,或者说猫这种类型是动物这种类型的子类型。函数
一下提到四个陌生的单词,不少同窗确定一下就懵了。React开发者应该对HOC (High Order Component) 不陌生,它就是使用一个基础组件做为参数,返回一个高阶组件的函数。React的基础是组件 (Component),在TypeScript里是类型 (Type),所以咱们用HOT (High Order Type) 来表示一个复杂类型,这个复杂类型接收一个泛型参数,返回一个复合类型。post
下面我用一个例子来阐述这四个概念,你能够将它使用TypeScript Playground运行,查看静态错误提示,进行更深入理解:
interface SuperType {
base: string;
}
interface SubType extends SuperType {
addition: string;
};
// subtype compatibility
let superType: SuperType = { base: 'base' };
let subType: SubType = { base: 'myBase', addition: 'myAddition' };
superType = subType;
// Covariant
type Covariant<T> = T[];
let coSuperType: Covariant<SuperType> = [];
let coSubType: Covariant<SubType> = [];
coSuperType = coSubType;
// Contravariant --strictFunctionTypes true
type Contravariant<T> = (p: T) => void;
let contraSuperType: Contravariant<SuperType> = function(p) {}
let contraSubType: Contravariant<SubType> = function(p) {}
contraSubType = contraSuperType;
// Bivariant --strictFunctionTypes false
type Bivariant<T> = (p: T) => void;
let biSuperType: Bivariant<SuperType> = function(p) {}
let biSubType: Bivariant<SubType> = function(p) {}
// both are ok
biSubType = biSuperType;
biSuperType = biSubType;
// Invariant --strictFunctionTypes true
type Invariant<T> = { a: Covariant<T>, b: Contravariant<T> };
let inSuperType: Invariant<SuperType> = { a: coSuperType, b: contraSuperType }
let inSubType: Invariant<SubType> = { a: coSubType, b: contraSubType }
// both are not ok
inSubType = inSuperType;
inSuperType = inSubType;
复制代码
咱们将基础类型叫作T
,复合类型叫作Comp<T>
:
Comp<T>
类型兼容和T
的一致。Comp<T>
类型兼容和T
相反。Comp<T>
类型双向兼容。Comp<T>
双向都不兼容。在一些其余编程语言里面,使用的是名义类型 Nominal type,好比咱们在Java中定义了一个class Parent
,在语言运行时就是有这个Parent
的类型。所以若是有一个继承自Parent
的Child
类型,则Child
类型和Parent
就是类型兼容的。可是若是两个不一样的class,即便他们内部结构彻底同样,他俩也是彻底不一样的两个类型。
可是咱们知道JavaScript的复杂数据类型Object,是一种结构化的类型。哪怕使用了ES6的class语法糖,建立的类型本质上仍是Object,所以TypeScript使用的也是一种结构化的类型检查系统 structural typing:
TypeScript uses structural typing. This system is different than the type system employed by some other popular languages you may have used (e.g. Java, C#, etc.)
The idea behind structural typing is that two types are compatible if their members are compatible.
所以在TypeScript中,判断两个类型是否兼容,只须要判断他们的“结构”是否一致,也就是说结构属性名和类型是否一致。而不须要关心他们的“名字”是否相同。
基于上面这点,咱们能够来看看TypeScript中那些“奇怪”的疑问:
首先咱们须要知道,函数这一类型是逆变的。
对于协变,咱们很好理解,好比Dog
是Animal
,那Array<Dog>
天然也是Array<Animal>
。可是对于某种复合类型,好比函数。(p: Dog) => void
却不是(p: Animal) => void
,反过来却成立。这该怎么理解?我这里提供两种思路:
假设(p: Dog) => void
为Action<Dog>
,(p: Animal) => void
为Action<Animal>
。
基于函数的本质
咱们知道,函数就是接收参数,而后作一些处理,最后返回结果。函数就是一系列操做的集合,而对于一个具体的类型Dog
做为参数,函数不只仅能够把它当成Animal
,来执行一些操做;还能够访问其做为Dog
独有的一些属性和方法,来执行另外一部分操做。所以Action<Dog>
的操做确定比Action<Animal>
要多,所以后者是前者的子集,兼容性是相反的,是逆变。
基于第三方函数对该函数调用
假设有一个函数F
,其参数为Action<Animal>
,也就是type F = (fp: Action<Animal>) => void
。咱们假设Action<Dog>
与Action<Animal>
兼容,此时咱们若是传递Action<Dog>
来调用函数F
,会不会有问题呢?
答案是确定的,由于在函数F
的内部,会对其参数fp
也就是(p: Animal) => void
进行调用,假设F
使用Cat
这一Animal
对其进行调用,若是此时咱们传递的参数fp
是(p: Dog) => void
,咱们自己是但愿其被调用时传递的参数p
是Dog
,然而fp
被调用时却使用了Cat
,这显然会使程序崩溃!
所以对于函数这一特殊类型,兼容性须要和其参数的兼容性相反,是逆变。
其次咱们再来看看为何TS里的函数还同时支持协变,也就是双向协变的?
前面提到,TS使用的是结构化类型。所以若是Array<Dog>
和Array<Animal>
兼容,咱们能够推断:
Array<Dog>.push
与Array<Animal>.push
兼容
(item: Dog) => number
和(item: Animal) => number
兼容
((item: Dog) => number).arguments
和((item: Animal) => number).arguments
兼容
Dog
和Animal
兼容为了维持结构化类型的兼容性,TypeScript团队作了一个权衡 (trade-off)。保持了函数类型的双向协变性。可是咱们能够经过设置编译选项--strictFunctionTypes true
来保持函数的逆变性而关闭协变性。
这个问题其实和函数类型逆变兼容一个道理,也能够用上述的两种思路理解,Dog
至关于多个参数,Animal
至关于较少的参数。
从第三方函数调用的角度,若是参数是一个非void的函数。则代表其不关心这个函数参数执行后的返回结果,所以哪怕给一个有返回值的函数参数,第三方的调用函数也不关系,是类型安全的,能够兼容。
一般状况下,咱们不须要构造名义类型。可是必定要实现的话,也有一些trick:
名义字符串:
// Strings here are arbitrary, but must be distinct
type SomeUrl = string & {'this is a url': {}};
type FirstName = string & {'person name': {}};
// Add type assertions
let x = <SomeUrl>'';
let y = <FirstName>'bob';
x = y; // Error
// OK
let xs: string = x;
let ys: string = y;
xs = ys;
复制代码
名义结构体:
interface ScreenCoordinate {
_screenCoordBrand: any;
x: number;
y: number;
}
interface PrintCoordinate {
_printCoordBrand: any;
x: number;
y: number;
}
function sendToPrinter(pt: PrintCoordinate) {
// ...
}
function getCursorPos(): ScreenCoordinate {
// Not a real implementation
return { x: 0, y: 0 };
}
// Error
sendToPrinter(getCursorPos());
复制代码
TypeScript的类型检测只是一种编译时的转译,编译后类型是擦除的,没法使用JavaScript的instanceof
关键字实现类型检验:
interface SomeInterface {
name: string;
length: number;
}
interface SomeOtherInterface {
questions: string[];
}
function f(x: SomeInterface|SomeOtherInterface) {
// Can't use instanceof on interface, help?
if (x instanceof SomeInterface) {
// ...
}
}
复制代码
若是要实现检测,须要咱们本身实现函数判断类型内部的结构:
function isSomeInterface(x: any): x is SomeInterface {
return typeof x.name === 'string' && typeof x.length === 'number';
function f(x: SomeInterface|SomeOtherInterface) {
if (isSomeInterface(x)) {
console.log(x.name); // Cool!
}
}
复制代码
还有更多“奇怪”的疑问,能够参考TypeScript Wiki FAQs。
最后来聊一下不变性 (Invariant) 的应用。上面咱们提到Array<T>
这一复合类型是协变。可是对于可变数组,协变并不安全。一样,逆变也不安全(不过通常逆变不存在于数组)。
下面这个例子中运行便会报错:
class Animal { }
class Cat extends Animal {
meow() {
console.log('cat meow');
}
}
class Dog extends Animal {
wow() {
console.log('dog wow');
}
}
let catList: Cat[] = [new Cat()];
let animalList: Animal[] = [new Animal()];
let dog = new Dog();
// covariance is not type safe
animalList = catList;
animalList.push(dog);
catList.forEach(cat => cat.meow()); // cat.meow is not a function
// contravariance is also not type safe, if it exist here
catList = animalList;
animalList.push(dog);
catList.forEach(cat => cat.meow());
复制代码
所以,咱们使用可变数组时应该避免出现这样的错误,在作类型兼容的时候尽可能保持数组的不可变性 (immutable)。而对于可变数组,类型本应该作到不变性。可是编程语言中很难实现,在Java中数组类型也都是可变并且协变的。