前言
TypeScript 中有许多关于类型系统的概念,如果只知其一不知其二的话,那么就有可能被报错打的满地找牙。
这篇文章写的是关于类型系统中的协变与逆变
的概念,了解协变和逆变是如何发生及运作的。
类型关系
理解一个新东西所需要的是一个良好且完善的上下文,所以需要先了解最基础的类型关系
。
在 TypeScript 中的类型只与值有关,即鸭子类型。
父子类型
普通类型
假设有如下接口类型:
interface Animal {
age: number
}
interface Dog extends Animal {
bark(): void
}
Dog
继承于父类 Animal
,也就是说 Dog
是 Animal
的子类型,我们可以称之为 Dog ≼ Animal
。
可以看到,子类相较于父类更具体,属性或行为更多。
同时可以看到因为鸭子类型
而出现的一个现象(同时也被称为类型兼容性
)。
let animal: Animal
let dog: Dog
animal = dog
// √ 因为 animal 只需要 age 一个属性,而 dog 中含有 age 和 bark() 两个属性,赋值给 animal 完全没问题。
dog = animal
// × Error: Property 'bark' is missing in type 'Animal' but required in type 'Dog'.
因为 animal
中缺少 dog
需要的 bark()
属性,因此赋值失败并报错。
总结:
- 子类型比父类型描述的更具体,父类型相对于子类型是更广泛的,子类型相对于父类型是更精确的。
- 判断是否是子类型可以这么理解,子类型是一定可以赋值给父类型的。
联合类型
假设有如下类型:
type Parent = 'a' | 'b' | 'c'
type Son = 'a' | 'b'
let parent: Parent
let son: Son
son = parent
// × Error: Type 'Parent' is not assignable to type 'Son'.
// Type '"c"' is not assignable to type 'Son'.
parent = son
// √
Parent
可能是 'c'
但是 Son
类型并不包括 'c'
这个字面量类型,因此赋值失败并报错。
可以从这个案例看出 Son ≼ Parent
。因为 Parent
更广泛,Son
更具体。
可以这么理解:联合类型相当于集合,Son
就是Prent
子集。不过在这还是说Son
是Parent
的子类型。
协变和逆变
维基百科定义
依旧假设我们有依旧有上面的Animal
和Dog
两个父子类型。
协变(Covariance)
协变的情况其实很简单就是上面说的类型兼容性,因此协变其实无处不在。
let animals: Animal[]
let dogs: Dog[]
animals = dogs
完全没问题,原因之前说了,就不再重复了。这就是协变
现象。
逆变(Contravariance)
逆变现象只会在函数类型中的函数参数上出现。 假设有如下代码:
let haveAnimal = (animal: Animal) => {
animal.age
}
let haveDog = (dog: Dog) => {
dog.age
dog.bark()
}
haveAnimal = haveDog
// Error: Type '(dog: Dog) => void' is not assignable to type '(animal: Animal) => void'.
// Types of parameters 'dog' and 'animal' are incompatible.
// Property 'bark' is missing in type 'Animal' but required in type 'Dog'.
haveAnimal({
age: 123,
})
传入的 Animal
没有 haveAnimal
需要的 bark()
属性,因此在检查时报错了。
注意:TS之前的函数参数是双向协变
的,也就是说既是协变
又是逆变
的、且这段代码并不会报错。但是在如今的版本 (Version 4.1.2)
在 tsconfig.json
中有 strictFunctionTypes
这个配置来修复这个问题。(默认开启)
那么这时候修改代码为:
- haveAnimal = haveDog
+ haveDog = haveAnimal
发现完全没问题!
因为我们在运行 haveDog
(实际运行还是 haveAnimal
) 的时候会传入 Animal
的子类Dog
,之前说过子类型的属性比父类型更多,因此haveDog
需要访问的属性在 Animal
中都有,那么在 Dog
类型中肯定只会更多。
可以发现对于两个父子类型作为函数参数构建两个函数类型,这两个的函数类型的父子关系逆转了,这就是逆变
。
同时,在返回值类型上和平常没什么区别是协变
的。(感兴趣的可以自己试试)
总结:在函数类型中,参数类型是逆变
的,返回值类型是协变
的。
练习
有如下代码:
type NoOrStr = number | string
type No = number
let noOrStr = (a: NoOrStr) => {}
let no = (a: No) => {}
noOrStr = no
会报错还是 no = noOrStr
会报错。
可以思考一下,谁是父类谁是子类然后在进行逆变转换。
练习答案
noOrStr = no
会报错。
解析:
- 在练习中,可以看做
No ≼ NoOrStr
,进行逆变转换:noOrStr ≼ no
。子类可以赋值给父类,父类不能赋值给子类,因此no = noOrStr
是对的没问题,noOrStr = no
就会报错。 - 又或者换种角度,
noOrStr
能处理number | string
类型的值,而no
只能处理number
类型的值。- 因此当
no = noOrStr
时没问题,因为调用no()
时只会传入number
类型的值,而noOrStr
可以处理包括number
两种类型的值。 - 而当
noOrStr = no
时就出问题了,因为调用noOrStr()
时会传入number | string
类型,而no
只能处理number
类型的值,当调用noOrStr()
传入string
类型的值时,no
处理不了,因此报错。
- 因此当
结语
这篇文章的感悟是我学习 TS 途中遇到的一个问题查询资料并理解后所诞生的。如果有错误或疏漏欢迎指出:)
同时扩展下:infer
在协变和逆变的情况下是有不同现象的,具体可查看文档(我忘记在哪里了)。
常见问题FAQ
- 免费下载或者VIP会员专享资源能否直接商用?
- 本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
- 提示下载完但解压或打开不了?
- 找不到素材资源介绍文章里的示例图片?
- 模板不会安装或需要功能定制以及二次开发?
发表评论
还没有评论,快来抢沙发吧!