再次彻底弄懂函数TS检查的逆变与协变

435 阅读6分钟

前言

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

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

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

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

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

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

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

什么是兼容性

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

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

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

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

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

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

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

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

入参数量上的兼容

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

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

变量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.理解逆变与协变.

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

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

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

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

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