0
点赞
收藏
分享

微信扫一扫

子类型、逆变、协变 Typescript

一ke大白菜 2022-04-13 阅读 105

前言

TypeScript 中有很多地方涉及到子类型 subtype、父类型 supertype、逆变和协变covariance and contravariance的概念,如果搞不清这些概念,那么很可能被报错搞的无从下手,或者在写一些复杂类型的时候看到别人可以这么写,但是不知道为什么他可以生效。(就是我自己没错了)

子类型

interface Animal {
  age: number
}

interface Dog extends Animal {
  bark(): void
}

在这个例子中,Animal 是 Dog 的父类,Dog是Animal的子类型,子类型的属性比父类型更多,更具体

  • 在类型系统中,属性更多的类型是子类型。
  • 在集合论中,属性更少的集合是子集。

子类型是父类型的超集,而父类型是子类型的子集,这是直觉上容易搞混的一点。
记住一个特征,子类型比父类型更加具体,这点很关键。

可赋值性 assignable

assignable 是类型系统中很重要的一个概念,当你把一个变量赋值给另一个变量时,就要检查这两个变量的类型之间是否可以相互赋值。

let animal: Animal
let dog: Dog

animal = dog // ✅ok
dog = animal // ❌error! animal 实例上缺少属性 'bark'

可赋值性角度来说,子类型是可以赋值给父类型的,也就是 父类型变量 = 子类型变量 是安全的,因为子类型上涵盖了父类型所拥有的的一切属性。

当我初学的时候,我会觉得 T extends {} 这样的语句很奇怪,为什么可以 extends 一个空类型并且在传递任意类型时都成立呢?当搞明白上面的知识点,这个问题也自然迎刃而解了。

在函数中的运用

假设我们有这样的一个函数以及俩个变量:

let val1 = { a: 1 }
let val2 = { a: 1, b: 2, c: 3 }

function f(val: { a: number; b: number })

f(val1) // err 缺少属性b

redux,在声明 dispatch 类型的时候,我就可以这样去做:

interface Action {
  type: string
}

declare function dispatch<T extends Action>(action: T)

这样,就约束了传入的参数一定是 Action 的子类型。也就是说,必须有 type,其他的属性有没有,您随意。

在联合类型中的运用

学习了以上知识点,再看联合类型的可赋值性,乍一看会比较反直觉,
'a' | 'b' | 'c''a' | 'b' 的子类型吗?它看起来属性更多诶?
其实正相反,‘a’ | ‘b’ | ‘c’ 是 ‘a’ | ‘b’ 的父类型。
因为前者比后者更「宽泛」,后者比前者更「具体」。

type Parent = 'a' | 'b' | 'c'
type Son = 'a' | 'b'

let parent: Parent
let son: Son

parent = son // ✅ok
son = parent // ❌error! parent 有可能是 'c'

这里 son 是可以安全的赋值给 parent 的,因为 son 的所有可能性都被 parent 涵盖了。

这个例子看完以后,你应该可以理解为什么 'a' | 'b' extends 'a' | 'b' | 'c'true 了,在书写 conditional types的时候更加灵活的运用吧。

逆变和协变

先来段维基百科的定义:

什么是逆变和协变

  • 协变(covariant),如果它保持了子类型序关系≦。该序关系是:子类型≦基类型。
  • 逆变(contravariant),如果它逆转了子类型序关系。

描述的比较晦涩难懂,但是用我们上面的动物类型的例子来解释一波,现在我们还是有 Animal 和 Dog 两个父子类型。

协变(Covariance)

那么想象一下,现在我们分别有这两个子类型的数组,他们之间的父子关系应该是怎么样的呢?没错,Animal[] 依然是 Dog[] 的父类型,对于这样的一段代码,把子类型赋值给父类型依然是安全的:

let animals: Animal[]
let dogs: Dog[]

animals = dogs

animals[0].age // ✅ok

转变成数组之后,对于父类型的变量,我们依然只会去 Dog 类型中一定有的那些属性。

那么,对于 type MakeArray<T> = T [] 这个类型构造器来说,它就是 协变(Covariance) 的。

逆变(Contravariance)

有这样两个函数:

let visitAnimal = (animal: Animal) => void;
let visitDog = (dog: Dog) => void;

animal = dog 是类型安全的,那么 visitAnimal = visitDog 好像也是可行的?其实不然,想象一下这两个函数的实现:

由于 visitDog 的参数期望的是一个更具体的带有 bark 属性的子类型,所以如果 visitAnimal = visitDog 后,我们可能会用一个不带 bark 属性的普通的 animal 类型来传给 visitDog。

visitAnimal = visitDog

let animal = { age: 5 }

visitAnimal(animal) // ❌

这会造成运行时错误,animal.bark 根本不存在,去调用这个方法会引发崩溃。
在对 Animal 和 Dog 类型分别调用如下的类型构造器之后:

type MakeFunction<T> = (arg: T) => void

父子类型关系逆转了,这就是 逆变(Contravariance)

举报

相关推荐

0 条评论