likes
comments
collection
share

typescript的协变逆变,相信我,这次肯定让你搞明白

作者站长头像
站长
· 阅读数 14

要搞清楚所谓的协变、逆变等概念,首先要整清楚什么是子类型 ---我说的

子类型

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;

我猜这个根本难不倒你对吧?我们稍作推理:

Obj1Obj2 中的 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中,通过类型构造器得到的新的类型,还是保持了原来的父子类型

Obj2Obj1 的子类型,和 Student 是 Person 的子类型关系保持一致。所以 Record 构造器是 协变

Arr2Arr1 的子类型,和 Student 是 Person 的子类型关系保持一致。所以 Array 构造器也是 协变

但是在推理3中,却反过来了

Fn1Fn2 的子类型,和 Student 是 Person 的子类型关系相反。所以函数的参数是 逆变

这个时候我们在看下wiki上的定义

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

简单的理解就是,一对具有父子关系的类型,通过类型构造器构造后的类型,是否还有父子关系。如果还是和之前一致,就是协变。相反则是逆变。

为什么说函数是双向协变

双向协变 就是 协变 + 逆变

按照刚才我们的推理,函数的参数应该是逆变的才对。怎么又来一个协变呢?

typescript的协变逆变,相信我,这次肯定让你搞明白 这其实是最开始一个权衡的结果。

那么为什么一开始函数的参数要设计成双向协变呢?官方解释

这个其实从上面的推理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 标记来修复这个问题。开启后函数参数就只是逆变了

留个坑,函数属性和函数方法存在差异哦

完结散花~