TypeScript里函数也能做类型兼容判断吗——协变和逆变

128 阅读8分钟

本文的代码均在严格模式下运行

前言

我们使用TS的大部分情况下,都只是判断两个对象/基本类型是否兼容,很少有个关于函数兼容是如何判断的。在判断函数兼容的过程中,又涉及到 协变(covariance)逆变(contravariance) 的概念,可能很多朋友觉得难以理解,我学的时候也一样。今天我想用通俗一点的方式说出来自己的理解,从实际例子出发,引出协变和逆变的概念。希望和大家一起探讨,如有错误也希望大佬指正。

interface Animal {}

interface Dog extends Animal {
  bark: () => void
}

interface Corgi extends Dog {
  shortLeg: true
}

现在我们声明了3个类型,很明显Corgi extends Dog extends Animal,这逻辑也很简单——柯基一定是狗,狗一定是动物,但是反过来不成立,因为动物不都是狗,也不是所有的狗都腿短(狗头)。

那么进入正题

给函数赋值成另一个函数

当参数类型不同时会发生什么

现在有下面3个函数:

declare let f1: (arg: Animal) => void
declare let f2: (arg: Dog) => void
declare let f3: (arg: Corgi) => void

f3、f2、f1分别接受Corgi、Dog、Animal为参数。那按理来说既然有Corgi extends Dog extends Animal,就应该能同样判断出来f3 extends f2 extends f1。这种情况下如果我们令f1 = f2或者f2 = f3的话肯定是成立的(能给f1赋值f2,说明f2一定是f1的子类型或者与f1相同类型)。

那接下来我们来试试是不是这样的:

f1 = f2 // Error Property 'bark' is missing in type 'Animal' but required in type 'Dog'.
f1 = f3 // Error Type 'Animal' is missing the following properties from type 'Corgi': shortLeg, bark
f2 = f3 // Error Property 'shortLeg' is missing in type 'Dog' but required in type 'Corgi'.
f2 = f1
f3 = f1
f3 = f2

好像有点问题?

鼠标移上去f1 = f2的错误可以看到 image.png f2中的参数Dog是需要bark属性的,而f1中的Animal并没有,毕竟狗会叫,动物不一定会叫。

当返回值类型不同时会发生什么

ok,姑且算你合理。那接下来我们改为给返回值设定类型的话表现应该是一样的吧。

declare let g1: (arg: unknown) => Animal
declare let g2: (arg: unknown) => Dog
declare let g3: (arg: unknown) => Corgi

再来看看会不会有报错

g1 = g2
g1 = g3
g2 = g3
g2 = g1 // Error Type 'Animal' is not assignable to type 'Dog'.
g3 = g1 // Error Type 'Animal' is not assignable to type 'Corgi'.
g3 = g2 // Error Type 'Dog' is not assignable to type 'Corgi'.

怎么还有问题?

我们需要搞明白为什么分别给函数的返回值参数定义类型会导致类型兼容的结果完全相反。

仔细观察f1 = f2g2 = g1的报错

declare let f1: (arg: Animal) => void
declare let f2: (arg: Dog) => void
declare let g1: (arg: unknown) => Animal
declare let g2: (arg: unknown) => Dog

f1 = f2 // Error Property 'bark' is missing in type 'Animal' but required in type 'Dog'.
g2 = g1 // Error Type 'Animal' is not assignable to type 'Dog'.

f1 = f2的操作中,ts判断的逻辑是f1(left hand)的参数类型是不是f2(right hand)的参数类型的子类型; 在g2 = g1的操作中,ts判断的逻辑是g1(right hand)的返回值类型是不是g2(left hand)的返回值类型的子类型

left hand即等式左手边,right hand即等式右手边,一般等式右手边会叫expression(表达式),但我想用right hand,显得直观

好,那现在我们有一个结论

TS严格模式下,给函数赋值为另一个函数时:

  1. TS会去判断left hand的参数类型是否是right hand的参数类型的subType(子类型)

  2. TS会去判断left hand的返回值类型是否是right hand的返回值类型的superType(父类型)

请各位思考一下,赋值和extends一样吗?

如果let a = b成立的话,那么是a extends b还是b extends a?如果是下面这种情况的话,Res等于1还是2?为什么?

type AnimalArg = (arg: Animal) => void
type DogArg = (arg: Dog) => void

type Res = AnimalArg extends DogArg ? 1 : 2 // ?

习题

接下来请做几个习题(不想做的话也可以直接跳到协变和逆变

type AnimalArg = (arg: Animal) => void
type DogArg = (arg: Dog) => void
type CorgiArg = (arg: Corgi) => void

type Res11 = AnimalArg extends AnimalArg ? 1 : 2 // ?
type Res12 = AnimalArg extends DogArg ? 1 : 2 // ?
type Res13 = AnimalArg extends CorgiArg ? 1 : 2 // ?

type Res21 = DogArg extends AnimalArg ? 1 : 2 // ?
type Res22 = DogArg extends DogArg ? 1 : 2 // ?
type Res23 = DogArg extends CorgiArg ? 1 : 2 // ?

type Res31 = CorgiArg extends AnimalArg ? 1 : 2 // ?
type Res32 = CorgiArg extends DogArg ? 1 : 2 // ?
type Res33 = CorgiArg extends CorgiArg ? 1 : 2 // ?

Res分别都是什么呢?

小提示: let str: string = 'nihao' 'nihao' extends string

再来做个返回值类型的题

type AnimalReturn = (arg: unknown) => Animal
type DogReturn = (arg: unknown) => Dog
type CorgiReturn = (arg: unknown) => Corgi

type Resu11 = AnimalReturn extends AnimalReturn ? 1 : 2 // ?
type Resu12 = AnimalReturn extends DogReturn ? 1 : 2 // ?
type Resu13 = AnimalReturn extends CorgiReturn ? 1 : 2 // ?

type Resu21 = DogReturn extends AnimalReturn ? 1 : 2 // ?
type Resu22 = DogReturn extends DogReturn ? 1 : 2 // ?
type Resu23 = DogReturn extends CorgiReturn ? 1 : 2 // ?

type Resu31 = CorgiReturn extends AnimalReturn ? 1 : 2 // ?
type Resu32 = CorgiReturn extends DogReturn ? 1 : 2 // ?
type Resu33 = CorgiReturn extends CorgiReturn ? 1 : 2 // ?

答案在下面


分割线


分割线


分割线


分割线


type Res11 = AnimalArg extends AnimalArg ? 1 : 2 // 1
type Res12 = AnimalArg extends DogArg ? 1 : 2 // 1
type Res13 = AnimalArg extends CorgiArg ? 1 : 2 // 1

type Res21 = DogArg extends AnimalArg ? 1 : 2 // 2
type Res22 = DogArg extends DogArg ? 1 : 2 // 1
type Res23 = DogArg extends CorgiArg ? 1 : 2 //1 

type Res31 = CorgiArg extends AnimalArg ? 1 : 2 // 2
type Res32 = CorgiArg extends DogArg ? 1 : 2 // 2
type Res33 = CorgiArg extends CorgiArg ? 1 : 2 // 1

// -----------------------------------------------------------------------------------

type Resu11 = AnimalReturn extends AnimalReturn ? 1 : 2 // 1
type Resu12 = AnimalReturn extends DogReturn ? 1 : 2 // 2
type Resu13 = AnimalReturn extends CorgiReturn ? 1 : 2 // 2

type Resu21 = DogReturn extends AnimalReturn ? 1 : 2 // 1
type Resu22 = DogReturn extends DogReturn ? 1 : 2 // 1
type Resu23 = DogReturn extends CorgiReturn ? 1 : 2 // 2

type Resu31 = CorgiReturn extends AnimalReturn ? 1 : 2 // 1
type Resu32 = CorgiReturn extends DogReturn ? 1 : 2 // 1
type Resu33 = CorgiReturn extends CorgiReturn ? 1 : 2 // 1

习题总结

想必经历了上面的习题之后,不管是extends还是赋值你应该都能轻松应对了,这时候我们再来总结一下(其实赋值跟extends并不需要分开总结,理解了一个自然就理解另外一个了,只不过总结一下方便记忆)

赋值时

  1. ts会去判断left hand的参数类型是否是right hand的参数类型的subType(子类型)
  2. ts会去判断left hand的返回值类型是否是right hand的返回值类型的superType(父类型)

举例

declare let f1: (arg: Animal) => void
declare let f2: (arg: Dog) => void
declare let g1: (arg: unknown) => Animal
declare let g2: (arg: unknown) => Dog

f1 = f2 // Error Property 'bark' is missing in type 'Animal' but required in type 'Dog'.
g2 = g1 // Error Type 'Animal' is not assignable to type 'Dog'.

extends时

  1. ts会去判断left hand的参数类型是否是right hand的参数类型的subType(父类型)
  2. ts会去判断left hand的返回值类型是否是right hand的返回值类型的superType(子类型)
type AnimalArg = (arg: Animal) => void
type DogArg = (arg: Dog) => void
type AnimalReturn = (arg: unknown) => Animal
type DogReturn = (arg: unknown) => Dog

type Res11 = AnimalArg extends DogArg ? 1 : 2 // 1
type Res12 = DogArg extends AnimalArg ? 1 : 2 // 2
type Res21 = AnimalReturn extends DogReturn ? 1 : 2 // 2
type Res22 = DogReturn extends AnimalReturn ? 1 : 2 // 1

协变和逆变

在上文中,我们能看到判断函数兼容时 参数返回值 的类型推导方向是完全相反的。

  • 参数:子类型逆变
  • 返回值: 子类型协变

什么意思呢?我们一起用段简单的代码来看一看,毕竟经过了上面的习题地狱后,想必这段代码你已经没有任何问题了。

type AnimalArg = (arg: Animal) => void
type DogArg = (arg: Dog) => void
type AnimalReturn = (arg: unknown) => Animal
type DogReturn = (arg: unknown) => Dog

type Res11 = AnimalArg extends DogArg ? 1 : 2 // 1
type Res12 = DogArg extends AnimalArg ? 1 : 2 // 2
type Res21 = AnimalReturn extends DogReturn ? 1 : 2 // 2
type Res22 = DogReturn extends AnimalReturn ? 1 : 2 // 1

这个过程是怎么体现协变和逆变的呢?

借用维基百科的一段定义

covariant if it preserves the ordering of types (≤), which orders types from more specific to more generic: If A ≤ B, then I<A> ≤ I<B>;
contravariant if it reverses this ordering: If A ≤ B, then I<B> ≤ I<A>;

看不看得懂英文不重要,就看代码

对于协变covariance,如果A是B的子类型,那么经过包装后的I<A>也是I<B>的子类型

做了这么久的题,有没有发现其实AnimalReturn其实就是Animal的包装类型?

If `A <= B`, then `I<A> <= I<B>`

type FuncReturn<T> = (arg: unknown) => T
type AnimalReturn = FuncReturn<Animal> // (arg: unknown) => Animal
type DogReturn = FuncReturn<Dog> // (arg: unknown) => Dog

type Res21 = AnimalReturn extends DogReturn ? 1 : 2 // 2
type Res22 = DogReturn extends AnimalReturn ? 1 : 2 // 1

逆变contravariance则相反,如果A是B的子类型,那么经过包装后的I<A>也是I<B>的子类型

If `A <= B`, then `I<B> <= I<A>`

type ArgReturn<T> = (arg: T) => void
type AnimalArg = FuncReturn<Animal> // (arg: Animal) => Animal
type DogArg = FuncReturn<Dog> // (arg: Dog) => Dog

type Res11 = AnimalArg extends DogArg ? 1 : 2 // 1
type Res12 = DogArg extends AnimalArg ? 1 : 2 // 2

最后,本文的出发点是讲一讲自己的理解,希望和大家一起探讨,如有错误也希望大佬在评论区留言指正。

参考资料

  1. 掘金小册 juejin.cn/book/708640…
  2. TS文档 www.typescriptlang.org/docs/handbo…
  3. 维基百科 en.wikipedia.org/wiki/Covari…