逆变与协变


啰嗦但大概率能用 –再次彻底弄懂函数TS检查的逆变与协变

前言

TypeScript 官网过于简单,对于有后端基础的前端同学来说,可能会看得犯困。

但兼容性这点,TS官方确实算是另辟蹊径,自开山门,允许你有一些”偷懒”,”不正确”的行为。

函数类型的变量约束 具有的 逆变特性,更是让人很难想象 规则制定者的精神高度。

能理解 兼容性, 再学点 内置条件类型,之后,TypeScript 就没有什么坎是过不去的……

TypeScript函数检查的逆变与协变,属于比较”偏门”的知识点,

却是TS学习者,不可缺少,却难以逾越的一道天堑。

当然,TypeScript 同样是 前端学习者,不可缺少,却难以逾越的一道天堑……

什么是兼容性

就是说一个被约束了类型的 变量 ,可以兼容其他类型的 __值__,

就像 一个被约束类型为父类的 变量 依旧可以被 赋值 为子类的实例

(因为子类必然具有父类的所有属性,这样调用时按父类的标准来调用是安全的)。

被 基本类型 接口类型 类类型 函数类型 泛型 约束的 变量,

均存在不同的兼容性,来放宽值的类型要求。

这次想要解释的就是 函数兼容性

而 函数的约束,主要体现在 入参返回值 上,其兼容性也是如此。

理解什么是 入参数量上的兼容,能更好的理解 函数类型变量 逆变和协变 的合理性

入参数量上的兼容

  1. 变量中 函数类型的约束,为 入参返回值

  2. 其中, 入参数量的减少可以被兼容(但类型依旧要相同,后文省略)

举个很常见的例子 [].forEach() 提供了三个参数,传入的callback函数,可以只用一个。

变量sum 类型被设置为 (a:number, b:number)=>number,

不考虑兼容性的请客下只有 f1 符合 变量sum的类型,

而实际应用中,却发现f2 f3 也可以被赋值给sum

type sumFunc = (a:number, b:number)=>number;
let sum:sumFunc;
function f1(a:number, b:number):number{
  return a+b;
}
//可以省略一个参数
function f2(a:number):number{
   return a;
}
//可以省略二个参数
function f3():number{
    return 0;
}
 //多一个参数可不行
function f4(a:number, b:number, c:number){
    return a+b+c;
}
sum = f1;
sum = f2;
sum = f3;
sum = f4; // 报错

sum(1,2) // 被TS要求必须传入两个number入参

在调用处,TS根据其类型sumFunc要求必须传入两个number入参,sum(1,2)

在调用处,TS不关心,sum的真实值究竟是哪个 f函数,只关心入参.

在知道调用时只给两个参数的情况下,在赋值处,TS就需要约束好,sum代表的函数,

那么需要三个参数的f4:(a:number, b:number, c:number) => number,很显然不能给sum.

f1:(a:number, b:number) => number, f1完全符合,肯定可以赋值

f2:(a:number) => numberf3:() => number, 则符合 函数兼容性, 可以赋值.

f2,被传了两个参数,仅仅是用不到第二个参数而已,函数不会因此报错,对吧?

所以很安全,所以允许 变量sum:sumFunc兼容 值f2 f1

因为 兼容性 的存在,只要是能安全执行,即使不符合 类型sumFunc的定义,

TS认为 入参数量减少 是安全的,是兼容的,所以也给了通过,允许将值f2 赋值给 变量sum

这就是 为什么允许 入参数量减少 被兼容。

逆变也是类似的思维

TypeScript函数检查的逆变与协变原理

要理解 __逆变与协变__,

  1. 首先要明确,type约束,实际上是约束 变量 可以接收哪种类型的

  2. 其次要理解,赋值语句,当 被约束的变量其被赋予的值 存在type差异时,TS将报错,

    但在满足 兼容性 要求的情况下,一定范围内的type差异,是被允许而不报错的。

  3. 对于函数变量来说,存在 赋值 和 调用,两处类型检查。

赋值处检查,变量type 与 形参type 的兼容性

调用处检查,变量type 与 实参type 的兼容性

注意! 逆变与协变 是存在于 赋值处的 兼容性检查,

而要理解 逆变与协变 的合理性,则要站在 调用处 的类型检查角度 来感受。

而难以理解的点在于:

  1. 允许差异(兼容性)的范围判断标准是什么? 是否安全

  2. 对于函数type来说,这个兼容性规则是什么? 入参数量减少的值 & 符合逆变与协变规则的值

  3. 什么是逆变与协变?入参要求少能理解,为什么说逆变与协变也安全?

什么是逆变与协变?

入参类型的兼容(逆变) 返回值类型的兼容(协变)

在给限定了 函数类型的变量 赋值函数时

赋值给 变量的 函数

其参数 可以是 比变量类型定义里 要求的属性 更少(逆变)

返回值 可以是 比变量类型定义里 要求的属性 更多(协变)

简化版:

__给变量赋值的函数 参数可以属性更少,返回值可以属性更多__。

理解版:

用于赋值的函数的 参数 的属性 必须 比 被赋值的变量的要求 更少

用于赋值的函数的 返回值 的属性 必须 比 被赋值的变量的要求 更多

返回值的协变很好理解,这里主要讲逆变

为什么满足 逆变 会安全

class Parent {
    house() { }
}
class Child extends Parent {
    car() { }
}
class Grandson extends Child {
    sleep() { }
}

// error: 赋值处,(逆变) 这里赋值 入参可以 Parent 或 Child, 不能 Grandson
const fun1: (arg: Child) => Child = (arg: Grandson): Grandson => {
    return new Grandson()
}
// error:调用处,这里入参可以是 Grandson 或 Child,不能 Parent
fun1(new Parent())

// 正解,值的入参type 只能 小于等于 fun2 的type要求
const fun2: (arg: Child) => Child = (arg: Child): Grandson => {
    return new Grandson()
}
// 正解,调用处的入参 只能 大于等于 fun2 的type要求
fun2(new Grandson())

调用处TS检查,会要求 入参type,属性数大于等于 变量type的入参约束。

(传入的属性更多,多余的属性用不到,但是该有的属性都有,很安全!)

赋值处TS检查,会要求 值type的入参type,属性数小于等于 变量type的入参约束(逆变)

变量type 由此有了一个 承上启下 的作用,

形参type <= 变量type的入参约束(逆变) <= 实参type

调用处的TS检查已经保证了第二个<=,赋值处存在 逆变 则保证了第一个 <=

如此则保证了,入参属性是满足函数的调用要求的,那么该次调用就必然是安全的。

结束语

本文涉及的知识点:

1.什么是兼容性.

2.函数变量兼容性的两种情况.

3.理解逆变与协变.

逆变确实很难理解,之前写过一篇逆变与协变的文章,现在去看也有点蒙,遂写了一篇新的。

看到这里再回去看开头的总结,应该能体会到逆变的逻辑吧?

本文自己读了很多遍,且基于自己上一篇文章进行改动,已经尽量解释,尽量精简。

日常熬夜,感谢点赞,欢迎讨论。

寒冬已来,大家应该加紧 “囤货” 啊!


文章作者: 罗紫宇
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 罗紫宇 !
  目录