typescript的协变逆变,相信我,这次肯定让你搞明白
要搞清楚所谓的协变、逆变等概念,首先要整清楚什么是子类型 ---我说的
子类型
在 OOP
中,如果一个 类B 继承 类A,则称 B 是 A 的子类型。划重点( B 是 A 的子类型 )
在 TS
中,也是如此
interface Person {
name: string
age: number
}
// Student 是 Person 的子类型
interface Student extends Person {
status: string
}
这里提醒一点,TS 中子类型的判断并非是通过 extends
来的,而是使用的 鸭子类型
来判断的。
所以下面也成立
interface Person {
name: string
age: number
}
// Student 还是 Person 的子类型
interface Student {
status: string
name: string
age: number
}
这里我们可以归纳总结以下特性:
- 首先 Person(人) 是一个更
宽泛
的概念,Student(学生) 是一个更具体
的概念。 - Student(学生) 肯定是 Person(人)。 反过来 人 不一定是学生。
- 从赋值上讲,Student 类型的值可以赋值给 Person,但是反过来是不对的
比如:
const haoza = new Student()
// ❌ error,此处会报错!
const people: Person = haoza
上面如果理解了,那么请回答下面的问题:
字符串和字符类型
let str: string = '我是字符串类型'
let haoza = '我是具体字符串' as const
- 请问
haoza = str
会报错吗? - 请问
str = haoza
会报错吗? - 请问 谁是谁的 子类型
首先我们要明确 谁宽泛,谁具体
。
很明显 string 是宽泛的。所以 haoza 是 str 的子类型
自然 `haoza = str` 会报错
联合类型
type union1 = string | number | boolean;
type union2 = string | number;
let test1 = 1 as union1;
let test2 = 1 as union2;
- 请问
test2 = test1
会报错吗? - 请问
test1 = test2
会报错吗? - 请问 谁是谁的 子类型
还是要先明确 谁宽泛,谁具体
。
很明显 union1 是宽泛的。 因为他的可能性更多。
union2 则是更具体。所以 union2 是 union1 的子类型
自然 `test2 = test1` 会报错
推理游戏
通过上面的学习,我猜你已经大概可能明白子类型了。
现在我们来玩一个推理游戏~
推理1,对象
已知:Person 和 Student 的父子类型关系,请问:
Record<string, Person>
和 Record<string, Student>
的父子类型关系?
或者回答下面问题
type Obj1 = Record<string, Person>;
type Obj2 = Record<string, Student>;
let test1 = {} as Obj1;
let test2 = {} as Obj2;
// 请问会报错吗❓
test2 = test1;
啥,太难了看不懂?行,换个简单的
type Obj1 = {
value: Person
}
type Obj2 = {
value: Student
}
let test1 = {} as Obj1;
let test2 = {} as Obj2;
// 请问会报错吗❓
test2 = test1;
我猜这个根本难不倒你对吧?我们稍作推理:
Obj1
和 Obj2
中的 key(键)
是相同的,那么只需要对比他们的值类型
。值类型单独拿出来比较,就是比较 Person 和 Student。
结论自然是会报错,ok 下一题下一题
推理2,数组
type Arr1 = Array<Person>;
type Arr2 = Array<Student>;
let test1 = [] as Arr1;
let test2 = [] as Arr2;
// 请问会报错吗❓
test2 = test1;
这这这和对象不是一样吗?稳了稳了。
答案自然是报错
嘿嘿嘿,你看我笑的多开心
推理3,函数
敲重点,函数一直是难点哦~
type Fn1 = (value: Person) => void
type Fn2 = (value: Student) => void
let test1: Fn1 = (value: Person) => {}
let test2: Fn2 = (value: Student) => {}
// 请问会报错吗❓
// 敲黑板,这儿是函数本身赋值,把test1赋值给test2,别看错题目了
test2 = test1;
是不是感觉无从下手了,该怎么分析呢?
首先,test2的类型是 Fn2 ,从Fn2的定义可以知道,他是需要 Student 的参数。那么我们调用 test2的时候,就必须传入Student。
假设 我现在调用 test2
test2(new Student())
此时 test2 真正的实现是 test1,所以全等于 调用 test1
而 test1 的参数是 Person
,但是我给了 Student
。函数内部会不会报错呢?肯定不会,因为 Student
上有 Person
的所有属性。
分析完毕,答案是 不会报错!
稍作总结
从上面三个例子,我已经告诉你什么是协变、什么是逆变了
推理1和推理2中,通过类型构造器得到的新的类型,还是保持了原来的父子类型
Obj2
是 Obj1
的子类型,和 Student 是 Person 的子类型关系保持一致。所以 Record
构造器是 协变
的
Arr2
是 Arr1
的子类型,和 Student 是 Person 的子类型关系保持一致。所以 Array
构造器也是 协变
的
但是在推理3中,却反过来了
Fn1
是 Fn2
的子类型,和 Student 是 Person 的子类型关系相反。所以函数的参数是 逆变
的
这个时候我们在看下wiki上的定义
协变与逆变(covariance and contravariance)是在计算机科学中,描述具有父/子型别关系的多个型别通过型别构造器、构造出的多个复杂型别之间是否有父/子型别关系的用语。
简单的理解就是,一对具有父子关系的类型,通过类型构造器构造后的类型,是否还有父子关系。如果还是和之前一致,就是协变。相反则是逆变。
为什么说函数是双向协变
双向协变
就是 协变 + 逆变
按照刚才我们的推理,函数的参数应该是逆变的才对。怎么又来一个协变呢?
这其实是最开始一个权衡的结果。
那么为什么一开始函数的参数要设计成双向协变呢?官方解释
这个其实从上面的推理2,我们可以反向推理出来
先问一个问题,数组是不是对象?当然是对吧。让我们来看看数组真正的定义
// 我只保留了部分
interface Array<T> {
/**
* Gets or sets the length of the array. This is a number one higher than the highest index in the array.
*/
length: number;
/**
* Returns a string representation of an array.
*/
toString(): string;
/**
* Appends new elements to the end of an array, and returns the new length of the array.
* @param items New elements to add to the array.
*/
push(...items: T[]): number;
/**
* Combines two or more arrays.
* This method returns a new array without modifying any existing arrays.
* @param items Additional arrays and/or items to add to the end of the array.
*/
[n: number]: T;
}
之前我们做数组的推理的时候,只针对 [n: number]: T
这部分做了对比。实际上,其他的属性也是需要对比的
length 和 toString 肯定是没问题的
问题在于 push 函数,如果它的参数只能是逆变的,那么数组是协变的推理就不能成立了。
所以这就是为什么函数的参数是双向协变的
现在你可以在 TypeScript 2.6 版本中通过 --strictFunctionTypes 或 --strict 标记来修复这个问题。开启后函数参数就只是逆变了
留个坑,函数属性和函数方法存在差异哦
完结散花~
转载自:https://juejin.cn/post/7234787766151266363