TypeScript 协变和逆变
前言
内涵和外延
说协变和逆变前先引入两个概念 内涵 和 外延
内涵: 概念中所反映的事物的特有属性 外延: 具有概念所反映的特有属性的所有事物
水果是指多汁且有甜味的植物果实,不但含有丰富的营养且能够帮助消化。水果是对部分可以食用的植物果实和种子的统称。这个是内涵。 它的外延包括了一切符合定义的事物,如:苹果,梨子,香蕉
内涵越小的概念, 覆盖的范围越多, 外延越多.
动物 => 狗 => 柴犬 => 幼年柴犬
相关资料
LSP(里氏替换原则)
- 子类可以实现父类的抽象方法, 但不能覆盖父类的非抽象方法
- 子类中可以增加自己特有的方法
- 当子类的方法重载父类的方法时, 方法的前置条件 (即方法的输入参数) 要比父类的方法更宽松
- 当子类的方法实现父类的方法时 (重写/重载或实现抽象方法), 方法的后置条件 (即方法的的输出/返回值) 要比父类的方法更严格或相等
定义
协变与逆变(Covariance and contravariance)是在计算机科学中,描述具有父/子型别关系的多个型别通过型别构造器、构造出的多个复杂型别之间是否有父/子型别关系的用语
可以先看看定义如果看得懂的话, 就不用往下看了, 反正我看定义是是看不懂.
👇 内容不一定正确, 主观加工, 错勿喷
协变与逆变是在类型系统中为更好的支持 LSP 带来特性
简单的描述 协变与逆变 就是两个类型之间关系的总结名词, 类似 A=[1,2,3], B=[1,2]. 我们说 B 是 A 的子集, 是对于现象的一种抽象总结,
下面我们来描述这种现象
后续 strictFunctionTypes 按照开启, 不讨论 ts 的双向变型
class Animal { base = '' }
class Dog extends Animal {
type = 'Dog'
}
// 会发生 协变与逆变 场景是 赋值 参数 返回值
// 协变: 类型收敛 内涵缩小 外延扩大
// 逆变: 类型外散 内涵扩大 外延缩小
// 赋值
let a: Animal
let b: Dog
a = new Dog // 发生了协变 类型收敛 Dog => Animal 内涵缩小了 外延扩大了
b = new Animal // error 不安全的 Property 'type' is missing in type 'Animal' but required in type 'Dog'.
// 参数
let fn1 = (animal: Animal) => {}
let fn2 = (dog: Dog) => {}
fn1 = fn2 // error 不安全的 Type '(dog: Dog) => void' is not assignable to type '(animal: Animal) => void'.
fn2 = fn1 // 发生了逆变 类型外散 Animal => Dog 内涵扩大了 外延缩小了
// 返回值
let fx1:() => Animal = () => new Animal
let fx2:() => Dog = () => new Dog
fx1 = fx2 // 发生了协变 类型收敛 Dog => Animal 内涵缩小了 外延扩大了
fx2 = fx1 // error 不安全的 Type '() => Animal' is not assignable to type '() => Dog'.
会发生 协变与逆变 场景 是 赋值 参数 返回值
通过上面现象, 我们可以总结出下面规律
赋值 是允许发生协变
函数返回值 是允许发生协变
函数参数 是允许发生逆变
理解
第一层
本质
协变与逆变是为了安全的使用类型转换
第二层
为什么会有这种现象
类型转换为了更好的满足面向对象编程, 继承, 多态, LSP 的使用, 如果严格限定了各种类型转换, 那么开发过程也就更加繁琐
第三层
这个东西是怎么实现的
实现上就是原本是 B 检测的时候然后把他的继承的类型也同时拿来比较验证, 并且符合安全转换条件的情况.
TypeScript 里具体实现 TypeScript-checker
可以参考 17847 行 checkTypeRelatedTo
方法 和 19419 行 mappedTypeRelatedTo
技巧
- 联合类型 转换成 交叉类型 Union to Intersection
type UnionToIntersection<U> = (U extends any ? (u: U) => void : never) extends (a:infer A) => any ? A : never
type cases = [
Expect<Equal<UnionToIntersection<'foo' | 42 | true>, 'foo' & 42 & true>>,
Expect<Equal<UnionToIntersection<(() => 'foo') | ((i: 42) => true)>, (() => 'foo') & ((i: 42) => true)>>,
]
通过 extends 将 'foo' | 42 | true
变成 'foo' => void | 42 => void | true => void
然后利用 函数参数 逆变 成 'foo' & 42 & true
从 ’foo‘
或 42
或 true
变成了 既是 ’foo‘
又是 42
还是 true
所以他的结果是 never
这个过程中每个函数都类型扩散了, 内涵扩大了 外延缩小了
- 元组提取函数返回值
class A{
a = 1
}
class B extends A{
b = 2
}
type FunToUnion<T extends any[]> = T[number] extends () => infer U ? U : never
FunToUnion<[() => string, () => number]> // string | number
FunToUnion<[() => A, () => B]> // A
B => A 发生了协变 类型收敛 内涵缩小了 外延扩大了
string 变成 string | number 我们明确的发现 外延扩大了, 所以 内涵一定缩小了, 发生了类型收敛