协变与逆变

子类型 在编程理论上是一个复杂的话题,而他的复杂之处来自于一对经常会被混淆的现象,我们称之为协变逆变。这两个单词看着就很高级,但是其实本质是一样的,它们都是为了做同一件事情,那就是类型安全。所以说只要我们理解了什么是内存安全,我们就可以理解什么叫协变和逆变。什么我们都不需要去管这两个概念,我们写出来的代码,就自然而然符合协变和逆变的规则。
什么是类型安全?其实说起来非常简单,在 TS 里面类型安全指的就是:

我们来举个🌰
我们有三种对象类型如下:

/**
 * 粉丝
 */
interface Fans {
  call: any;
}
/**
 * 爱坤
 */
interface IKun extends Fans {
  sing: any;
  dance: any;
  basketball: any;
}

/**
 * 超级爱坤
 */
interface SuperIKun extends IKun {
  rap: any;
}

比较这三种对象的类型,我们可以有一个大小的称呼,我们把类型约特殊则认为越小,那么上面这三个类型的关系就是:
SuperIKun < IKUn < Fans
那么现在说回到类型安全,假如我们有一个变量,类型是 Fans ,还有一个变量,类型是 IKun
那么 Fans 这个对象可以赋值给 IKun 吗?

const fans: Fans = {
  call: "",
};

const iKun: IKun = fans;
      // ERROR:类型“Fans”缺少类型“IKun”中的以下属性: sing, dance, basketball

很显然 IDE 帮我们报错了,提示我们不应该这样赋值,因为这样做就失去了类型安全。因为将来我们在用这个 iKun 的时候,由于它是 IKun 这个类型,所以它里面的成员我们应该都可以使用,比如 iKin.sing而我们给他的东西却没有这些成员,所以如果它不报错,就会造成我们在正常使用的时候就会出现我们使用到根本不可能出现的字段,从而导致空指针异常。所以这就是类型安全,它保证了成员的使用可用。
那么反过来,如果有一个 IKun,能不能把它赋值给 Fans 呢?

const iKun: IKun = {
  call: "",
  sing: "",
  dance: "",
  basketball: "",
};

const fans: Fans = iKun;

这样做就是可以的,因为 Fans 里面只有一个成员 call ,而 iKunFans 的一种,它是小于Fans 的,所以它里面不管有多少成员,它一定有一个成员 call ,因此我们在用这个 call 的时候它一定是可用的,可以保证它是安全的。这就叫作类型安全。
通过上面的例子我们可以发现,要保证类型安全,只要实现一点就可以了,就是要搞清楚谁在给这个数据,谁在收这个数据。我们只需要保证给的类型是个小类型,收的类型是个大类型,因为小的类型里面一定包含大的类型的成员,所以只要满足这个条件就没问题。
比如第一个例子是我们可以发现,是 Fans 在给,IKun 在收,而 Fans > IKun,不满足我们上面的结论,所以不符合类型安全。
第二个例子里,是 IKun 在给,Fans 在收,所有符合类型安全。
所以我们根本不需要知道什么是协变什么是逆变,我们只需要掌握一点即可,那就是:给的类型是个小类型,收的类型是个大类型。

那么在不同的场景里面,我们只要分清楚谁在给,谁在收,只要保证给的是小类型,收的是大类型,那么它一定是类型安全的。在上面这个简单的例子里,其实挺好分析的,但是有的时候可能没有那么直观得看出来。比如我们声明两个函数:

type Transform = (x: IKun) => IKun;
type SuperTransform = (x: SuperIKun) => SuperIKun;

然后我们有一个函数类型是 SuperTransform,那么我可以赋值给 Transform 吗?

type Transform = (x: IKun) => IKun;
type SuperTransform = (x: SuperIKun) => SuperIKun;

const subTransform: SuperTransform = (x) => {
  return x as any;
};
const transform: Transform = subTransform;
      //ERROR: 不能将类型“SuperTransform”分配给类型“Transform”

大家可能很奇怪,明明 SuperTransformTransform 小,为什么这样会报错了?难道刚才我们得到的结论是错误的吗?其实不是的,我们来分析下为什么会报错。报错的原因一定是它认为这样做可能会造成类型不安全,我们可以想,如果这样能赋值成功,那我们后续调用 transform 函数的时候,就可以以 IKun 作为参数,但是我们实际执行的确是 subTransform 方法,它的形参的类型是 SuperIKun,也就是它可以访问 rap 这个成员,但是我们传进来的 IKun 是没有这个成员的,所以产生了类型不安全。所以我们看一个这个函数的参数,是 IKun 在给,SuperIKun 在收,而 IKUn > SuperIKun。所以赋值失败,满足我们上面的结论。
所以当类型是函数的时候,我们不能比较这个函数类型的大小,因为函数是有参数和返回值的,所以我们需要同时比较参数和返回值。所以上面的例子中,我们把 SuperTransform 的参数类型改为 Fans 就可以了。因为 IKun < Fans

type Transform = (x: IKun) => IKun;
type SuperTransform = (x: Fans) => SuperIKun;

const subTransform: SuperTransform = (x) => {
  return x as any;
};
const transform: Transform = subTransform;

这样就不会有类型安全的问题了。
我们再把上面的例子改变一下,我们把 SuperTransform 函数的返回值类型改为 Fans

type Transform = (x: IKun) => IKun;
type SuperTransform = (x: Fans) => Fans;

const subTransform: SuperTransform = (x) => {
  return x as any;
};
const transform: Transform = subTransform;
      //ERROR: 不能将类型“SuperTransform”分配给类型“Transform”。

这样类型又报错了。同样的我们来分析下返回值是谁在给谁在收。这个返回值是谁返回的?很明显是 SuperTransform 返回的,所以给的是 Fans,收的是谁,是调用 transform 得到的类型,是 IKun,所以是把 FansIKun。但是 Fans > IKUn 。不满足我们上面的结论,所以赋值失败。
关于协变与逆变,其实就是上面我们给的对象和函数的例子,对象的类型赋值符合我们直观的小给大,所以叫协变。而函数的赋值直观上是大给小,需要考虑参数和返回值,所以叫逆变。其实我们完全可以忘记协变和逆变这两个专有名词,只需要记住一个结论就够了: