TypeScript中的协变和逆变
- Published on
- Reading time
- 7 min read
- Likes
关于这个主题,有很多概念需要了解,接下来通过一些问题来理解父类型、子类型,以及集合和不可变性等的概念。
概念
关于子类型和父类型的概念其实容易搞混,我用通俗的话解释一下:在 TypeScript 的 class 中,子 class 继承父 class,然后添加了一堆的方法和属性等场景中:
class Animal {
name: string
}
class Cat extends Animal {
breed: string
age: string
}
在这里:
- 子类比父类拥有更多的属性。
- 子类比父类更具体(下面有一个例外情况)。
子类型和父类型之间还有一个重要的概念:父类型可以被子类型赋值。你可以理解为:
- 父类型是一个熵很高、模糊的集合,这个集合里的元素都是模棱两可的(ambiguous)。(PS: 人生来混沌,我们通过不断地学习和迭代,才能减小熵,让自己更完美。)
- 子类型是基于父类型的,通过更具象的属性来规范性地扩展(extends,&)父类型,而不是通过或(|)这种方式来扩大熵的增量。
因为在 TypeScript 的类型系统中,我们通常要求类型越窄越精准越好,这是因为类型越窄,TS 的语言系统才能更好地帮助我们检查出想要的问题。因此,我们可以得出一个结论:
子类型能
assign
赋予父类型。
题目1
解释一下以下代码为什么不会报错:
type Type1 = {}
type Type2 = {}
type NamedVariable = (Type1 | Type2) & { name: string }
const b: NamedVariable = {
name: 'eavan',
}
其实 Type1 和 Type2 是空对象,空对象里的键是随意的。
但是 { name: string }
和空对象类型取交集后,依然是 { name: string }
。
题目2
type A = 3 | 4 | 5
type B = 3 | 4
请问谁是子类型?
- 答案:B是A的子类型。
为什么呢?看起来 A 不是比 B 多一个可选类型吗?为什么 A 是父类型,而 B 是子类型?
因为这是一个联合类型。A 目前有三个可能性,而 B 只有两个可能性。这样来说,B 比 A 更加具体。
通过以上学习,你应该就能理解什么是协变了。
但什么是逆变呢?应该就是反过来的概念。那么什么场景下是逆变呢?
题目3
猜一猜 D 是什么类型:
type D = number[] extends readonly number[] ? true : false
答案是 true。
这里面涉及到不可变性和可变性的概念。
类型安全:当一个函数接受一个 readonly number[]
作为参数时,它可以假设数组不会被修改。这保证了函数的行为是可预测的和安全的。
赋值规则:由于 readonly number[]
只是增加了不可变性约束,而没有改变数组元素的类型,因此从类型系统的角度看,number[]
赋值给 readonly number[]
是安全的。虽然 number[]
更加宽泛,但它符合 readonly number[]
的所有约束。
逆变的场景
先说结果,逆变基本发生在函数的场景下。
你可以通过以上几个小例子去分清谁能 assign 谁,但对于以下几个类型你怎么 assign 呢?是不是很懵逼?
type A12 = 1 | 2
type B123 = 1 | 2 | 3
type FnA12 = (p: A12) => void
type FnB123 = (p: B123) => void
type CCC = FnA12 extends FnB123 ? true : false
猜一猜这时候 CCC 是 true 还是 false?
答案是 false。有人会说:不是 FnA12 更具体吗?明明参数只能选两个?
其实在函数里面,这套规则就不适用于之前所说的协变场景,并且这是函数的参数,维度又提升了一层,而不是我们之前比谁在谁的集合等这种小游戏。
那么我们要如何理解这种思维180°倒退的场景呢?
用逆向思维去解决这种问题:
- FnB123 的参数是 1 和 2,那么它只能处理传过来的参数为 1 或者 2 的场景。
- 但是 FnB123 就不一样了,它不管是 1 还是 2,甚至是 3,都能轻松应对。
- 那么是否可以说用前朝的剑(FnB123)就能斩本朝的官(当参数为 1 或者 2 时)?
- 这意味着 FnB123 能
assign
FnA12。
在函数中,参数类型是逆变的,因为如果一个函数能接受更泛化的类型,那么它也可以接受更具体的类型。这意味着一个接受所有可能参数(如 1、2、3)的函数,可以安全地替代只接受特定参数(如 1 或 2)的函数。
总结
在 TypeScript 中,协变允许使用比原本预期的类型更加具体的类型。反之,逆变允许使用比原本预期的类型更加泛化的类型。
这下基本能理解什么是协变,什么是逆变了。当然,TS 默认是双向协变的,你需要开启一个 strictFunctionTypes
的选项才能享受到这种逆变的快感。
本文采用CC BY-NC-SA 4.0 - 非商业性使用 - 相同方式共享 4.0 国际进行许可。