typescript难点:顺变和逆变

2,222 阅读4分钟

示例来源于:深入理解TypeScript

为了方便,约定A → B 指的是以 A 为参数类型,以 B 为返回值类型的函数类型。

我们先不谈论逆变与协变。看一下一个有趣的问题。

Greyhound (灰狗)是 Dog (狗)的子类,而 Dog 则是 Animal (动物)的子类。由于子类型通常是可传递的,因此我们也称 GreyhoundAnimal 的子类。

问题:以下哪种类型是 Dog → Dog 的子类呢?

  1. Greyhound → Greyhound
  2. Greyhound → Animal
  3. Animal → Animal
  4. Animal → Greyhound

以代码来演示,先定义类 。

class Animal {
    public speak() {
        console.log("Animal speak")
    }
}

class Dog extends Animal {
    public eatBone() {
        console.log("eat bone")
    }
}

class Grayhaund extends Dog {
    public catchRabbit(){
        console.log("catch Rabbit")
    }
}

class GermanShepherd extends Dog {
}
type TargetFunction = (dog: Dog) => Dog;
let fun: (g: TargetFunction) => any;

fun 是一个以 Dog → Dog 为参数的函数 。 上面fun函数并没有完整定义,下面将重点谈论fun的内容。

我们知道,g被传入fun,那它很可能被调用,此时g要求输入一个参数

fun = function(g){
    const dog = g(new Dog());
    const grayhaund = g(new Grayhaund());
    const germanShepherd = g(new GermanShepherd());
    
    //报错
    //类型“Animal”的参数不能赋给类型“Dog”的参数。
    const animal = g(new Animal())
}

我们看到, g的参数类型被限制为Dog及其子类。

接着,我们看返回类型

fun = function(g){
    const dog = g(new Dog());
    const dog2 = g(new Grayhaund());
    dog.speak();
    dog.eatBone();
    dog2.speak();
    dog2.eatBone();
    
    //报错
    //Dog上没有方法catchRabbit
    dog.catchRabbit();
    dog2.catchRabbit();
}

强调一点,g是未知的,g的所有返回值类型都被推断为Dog类型,与g的内容无关。

所以, g的返回值只能使用Dog及其超类(Animal)上的方法。

所以,fun定义的函数内容通过编译的必要条件:

  • g的参数类型被限制为Dog及其子类。
  • g的返回值只能使用Dog及其超类(Animal)上的方法。

回到开头,哪种会通过编译?

f(g)
//下面哪种不使f(g)报错?
//1. Greyhound → Greyhound
//2. Greyhound → Animal
//3. Animal → Animal
//4 .Animal → Greyhound

如果你认真看上面的分析,很快就能得出答案,第四种可以通过编译。

前提 fun 内容可以通过编译 则

参数类型一定是Dog及其子类,那么它也一定满足继承于Dog的超类(Animal)。

返回值只能使用Dog及其超类(Animal)上的方法,那么它是Dog的子类一定可以调用这些方法。

此时,我们称 返回值类型是协变的,而参数类型是逆变的。

A ≼ B 意味着 AB 的子类型。

返回值类型是协变的,意思是 A ≼ B 就意味着 (T → A) ≼ (T → B)

参数类型是逆变的,意思是 A ≼ B 就意味着 (B → T) ≼ (A → T)

其他类型

img

这是王垠博客提出的一个问题。

typescript中也有这个问题

let a:string[] = [];
//这么赋值并不报错
let b:Obeject[] = a;
//下面并不报错
b[0] = "123";
b[1] = 1;

(有个基础知识你需要知道,所有的基本类型都是在被包装到Object的原型链上的,可以用Object原型的任何方法,所以Object类型可以赋任何基础值。)

但是,这可能是typescript有意而为之。沿用上面示例的类

let a:Animal[] = [];
//b报错
//不能将类型“Animal[]”分配给类型“Dog[]”。
//  Property 'foo' is missing in type 'Animal' but required in type 'Dog'.ts(2322)
let b:Dog[] = a;

let c:Dog[] = [];
let d:Animal[] = c;

我们看到如果是按照逆变来赋值,是会报错的,说明typescript是检查这种情况的。

如果不报错,考虑下面这种情况。

let a:Animal[] = [];
let b:Dog[] = a;
a[0] = new Animal();
b[0].eatBone();

Duck类型没有eatBone方法 这就是unsafe了。

不过顺变也有这个问题

let c:Dog[] = [];
let d:Animal[] = c;

d[0] = new Animal();
c[0].eatBone();

Animal类上没有eatBone , 这也是 unsafe的。

该如何解决?

我们看一位大佬的解答(他的知乎)

它这个的设计错误在于,数组的索引器是一个输入输出的接口,可以看做一对getter setter方法。

setter方法接收的参数类型应该是逆变的。

这里a虽然能协变到b,但是a[]的参数类型a到b[]的参数类型a不是逆变关系

他的意思是,如果按上例赋值,b[0]在赋值时,应该限制在Dog类及其子类内,这样才能保证一定有方法调用。

不学typescript啦类型系统太复杂头发都掉光了哭唧唧。