一次关于TS逆变与协变的尝试
不知道各位平时在写TS的过程中有没有看到或听到过有关于【逆变】、【协变】这样的名词?反正我每每听到都是一头雾水,如果你也跟我一样,那实在是泰裤辣! 那就通过这篇文章来简单认识一下吧!
关于什么是逆变
,什么是协变
,维基百科给出的定义是:
协变与逆变(Covariance and contravariance)是在计算机科学中,描述具有父/子型别关系的多个型别通过型别构造器、构造出的多个复杂型别之间是否有父/子型别关系的用语。
什么是型别?上网查一下,竟然就是类型的意思。那么这句话虽然目前还看不懂,但起码获取到了一个有用的信息,那就是逆变
与协变
都与父子类型
有关。
父子类型
我们先从父子类型说起。
思考以下接口
interface Person {
name: string
}
interface Teacher extends Person {
teach(): void
}
我们定义了一个接口 Person
,并让另一个接口 Teacher
继承了 Person
,显而易见的,Teacher
有 Person
的所有属性,另外还有自己的属性 teach,那么,我们就认为 Person
是 Teacher
的父类型。
在类型系统中,子类型总是比父类型的属性更多,且更加具体;在集合论中,属性少的是子集,属性多的称为超集,因此也可以说,子类型是父类型的超集。
再思考以下类型
type A = 'a' | 'b' | 'c'
type B = 'a' | 'b'
我们定义了两个字面量类型的联合类型,根据上面的结论,我们可能很容易把 A 误认为是 B 的子类型,其实不然,A 比 B 更宽泛,B 比 A 更具体,B 其实是 A 的子类型。
我们从【可赋值性】这一角度来简单解释一下。
思考如下代码
let person: Person
let teacher: Teacher
teacher = person
person = teacher
我们把 person
赋值给 teacher
,即将一个父类型的变量赋值给一个子类型的变量,这是会报错的。ts 会告诉我们,变量 person 上缺少属性 “teach”。
但假如我们把 teacher 赋值给 person 就完全没有问题,因为 person 只要满足有 name
这一属性即可。
同样的,在联合类型中
let parent: A
let son: B
parent = son // 正确
son = parent // 错误,son可能会得到不期望的字面量 'c'
这就解释了为什么说 A 是 B 的父类型了。
如果是函数呢?
const func = (param: { a: number, b: number }) => {}
const p1 = { a: 1 }
const p2 = { a: 1, b: 2, c: 3 }
func(p1)
func(p2)
我们的 func(p1)
是会报错的,因为参数缺少属性 b
,在 func
函数中可能会使用 b
这个属性。
但 func(p2)
不会报错,因为我们的传参已经完全满足了 func 函数需要的参数,至于多出来的 c
已经不重要了。
逆变与协变
先说【协变】。
思考如下代码
let persons: Person[]
let teachers: Teacher[]
teachers = persons // 报错,persons 中缺少 teacher 所需要的 teach
persons = teachers // 正常
发现了吗,即使我们把他们的类型从 “对象” 拓展到了 “数组”,他们的父子关系仍然没有发生变化。
这就是【协变】。
回到维基百科给出的定义中所描述的 “型别构造器
”,对于上述例子这一【协变】来说,他的型别构造器就是 type MakeArr<T> = T[]
。
一切都说通了,我们将 Person
和 Teacher
分别带入泛型中,他们最后构造出的类型数组,其父子关系没有发生变化。
再说【逆变】。
有如下代码
const func1 = (person: Person) => {
console.log(person.name)
}
const func2 = (teacher: Teacher) => {
console.log(teacher.name)
teacher.teach()
}
既然 Person 是 Teacher 的父类型,那么是不是可以让 func1 = func2
呢?
答案是不行。想想看,我们在 func2
里还调用了传入参数的 teach
属性,如果我们把 func2
赋值给了 func1
,那么后续使用的时候,传入的参数类型为 Person
,再调用 teach
就会报错了!
那么 func2 = func1
可以吗?当然可以,因为 func2
的参数完全能够满足 func1
的需要。
发现了吗,父子类型的关系逆转了,这就是【逆变】。
因此,我们构造一个这样的“型别构造器
”:type MakeFunc<T> = (arg: T) => void
,对类型 Person
和 Teacher
调用此构造器,就将这两个类型逆变了。
总结
逆变与协变,其实描述的就是父子关系的变化。通过不同的类型构造器所生成的新的类型与原始类型关系的比较,仍然保持原来的关系就称之为协变,反之称其为逆变。
需要注意的是,在 TypeScript 中,由于灵活性等权衡,对于函数参数默认的处理是双向协变的,即 func1 = func2
是可以成立的。在开启了 tsconfig 中的 strictFunctionType 后才会严格按照 逆变 来约束赋值关系。
转载自:https://juejin.cn/post/7247027696822419515