从鸭子类型学逆变和协变

156 阅读3分钟

在了解Ts的逆变和协变之前,我们先了解一下什么叫做鸭子类型

鸭子类型

如果一只鸟走路像鸭子 ,游泳也像,做什么都像,那么这只鸟就可以成为鸭子类型。

我们不用关心鸭子的定义是什么,只要符合我们通常意义上的认知,那么他就是这个物体。

在 TypeScript中,只要对象符合定义的类型约束,那么我们就可以视为他是。

协变

协变,又可以称为鸭子类型。通常发生在检查这两个变量的类型之间是否可以相互赋值。

interface A {
    name:string
    age:number
}
 
interface B {
    name:string
    age:number
    sex:string
}
 
let a:A = {
    name:"老墨我想吃鱼了",
    age:33,
}
 
let b:B = {
    name:"老墨我不想吃鱼",
    age:33,
    sex:"女"
}
 
a = b

接口A中有name和age属性,接口B有name,age和sex属性。 当我们将b赋值给a的时候,因为B类型是A类型的子集,所以我们可以将B赋值给A

回到我们的鸭子类型上,我们可以将A理解成那个鸭子,B理解成那只鸟。因为B中的name像A,B中的age像A,即B满足A的所有特性,所以B就是A类型,可以正确赋值。换句大白话讲,就是B类型只能多,不能少,才可以正确赋值。

我们再从集合的角度来理解协变。刚刚我们提到B是A的子集,相信一定会有同学出来反驳,明明,B的属性比A多,A才是B的子集!

这里的集合概念和我们平时认知的不太一样。大家可以换个角度想,接口中的属性可以相当于我们的限制条件。就拿我们找工作举例子,熟悉React的应聘者是一个集合A,当我们增加我们的招聘要求时,不仅熟悉React,还要熟悉node,满足这两个招聘条件的应聘者又构成一个集合B。那么集合B是不是集合A的子集呢?

相信大家对协变已经有自己的理解了吧!

逆变

逆变一般发生于函数的参数上面

interface A {
    name:string
    age:number
}
 
interface B {
    name:string
    age:number
    sex:string
}
 
let a:A = {
    name:"老墨我想吃鱼了",
    age:33,
}
 
let b:B = {
    name:"老墨我不想吃鱼",
    age:33,
    sex:"女"
}
 
a = b
 
let fna = (params:A) => {
 
}
let fnb = (params:B) => {
    
}
 
fna = fnb //错误
 
fnb = fna //正确

我们刚刚说到B是A的子集, 但是我们在函数的参数中使用A/B类型,为什么和刚刚的结果不一样呢? 只能将fna赋值给fnb

小栗子又来了

我们在使用函数进行赋值的时候,fnb = fna 。当我们赋完值调用fnb时,实际上调用的是fna

let a = () => {
    console.log('a')
}

let b = () => {
    console.log(b)
}

b = a;
b() //a

所以fna中params:A又充当了我们的主类型,B是我们的子类型,B是A的子集,所以赋值成功!

双向协变

tsconfig中strictFunctionTypes 设置为false 支持双向协变 fna fnb 随便可以来回赋值,但是不推荐这么设置。

通过tsc --init生成配置项

fna = fnb
fnb = fna

image.png