TypeScript 协变和逆变

1,308 阅读4分钟

TypeScript 协变和逆变

前言

内涵和外延

说协变和逆变前先引入两个概念 内涵 和 外延

内涵: 概念中所反映的事物的特有属性 外延: 具有概念所反映的特有属性的所有事物

水果是指多汁且有甜味的植物果实,不但含有丰富的营养且能够帮助消化。水果是对部分可以食用的植物果实和种子的统称。这个是内涵。 它的外延包括了一切符合定义的事物,如:苹果,梨子,香蕉

内涵越小的概念, 覆盖的范围越多, 外延越多.

动物 => 狗 => 柴犬 => 幼年柴犬

相关资料

LSP(里氏替换原则)

  • 子类可以实现父类的抽象方法, 但不能覆盖父类的非抽象方法
  • 子类中可以增加自己特有的方法
  • 当子类的方法重载父类的方法时, 方法的前置条件 (即方法的输入参数) 要比父类的方法更宽松
  • 当子类的方法实现父类的方法时 (重写/重载或实现抽象方法), 方法的后置条件 (即方法的的输出/返回值) 要比父类的方法更严格或相等

定义

协变与逆变(Covariance and contravariance)是在计算机科学中,描述具有父/子型别关系的多个型别通过型别构造器、构造出的多个复杂型别之间是否有父/子型别关系的用语

协变与逆变 - 维基百科

可以先看看定义如果看得懂的话, 就不用往下看了, 反正我看定义是是看不懂.


👇 内容不一定正确, 主观加工, 错勿喷

协变与逆变是在类型系统中为更好的支持 LSP 带来特性

简单的描述 协变与逆变 就是两个类型之间关系的总结名词, 类似 A=[1,2,3], B=[1,2]. 我们说 B 是 A 的子集, 是对于现象的一种抽象总结,

下面我们来描述这种现象

后续 strictFunctionTypes 按照开启, 不讨论 ts 的双向变型

Playground

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

可以参考 17847checkTypeRelatedTo 方法 和 19419mappedTypeRelatedTo

技巧

  1. 联合类型 转换成 交叉类型 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‘42true 变成了 既是 ’foo‘ 又是 42 还是 true 所以他的结果是 never 这个过程中每个函数都类型扩散了, 内涵扩大了 外延缩小了

  1. 元组提取函数返回值
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 我们明确的发现 外延扩大了, 所以 内涵一定缩小了, 发生了类型收敛

参考