啰嗦但大概率能用 –再次彻底弄懂函数TS检查的逆变与协变
前言
TypeScript 官网过于简单,对于有后端基础的前端同学来说,可能会看得犯困。
但兼容性这点,TS官方确实算是另辟蹊径,自开山门,允许你有一些”偷懒”,”不正确”的行为。
函数类型的变量约束 具有的 逆变特性,更是让人很难想象 规则制定者的精神高度。
能理解 兼容性, 再学点 内置条件类型,之后,TypeScript 就没有什么坎是过不去的……
TypeScript函数检查的逆变与协变,属于比较”偏门”的知识点,
却是TS学习者,不可缺少,却难以逾越的一道天堑。
当然,TypeScript 同样是 前端学习者,不可缺少,却难以逾越的一道天堑……
什么是兼容性
就是说一个被约束了类型的 变量 ,可以兼容其他类型的 __值__,
就像 一个被约束类型为父类的 变量 依旧可以被 赋值 为子类的实例 值
(因为子类必然具有父类的所有属性,这样调用时按父类的标准来调用是安全的)。
被 基本类型 接口类型 类类型 函数类型 泛型 约束的 变量,
均存在不同的兼容性,来放宽值的类型要求。
这次想要解释的就是 函数兼容性
而 函数的约束,主要体现在 入参 和 返回值 上,其兼容性也是如此。
理解什么是 入参数量上的兼容,能更好的理解 函数类型变量 逆变和协变 的合理性
入参数量上的兼容
变量中 函数类型的约束,为 入参 和 返回值
其中, 入参数量的减少可以被兼容(但类型依旧要相同,后文省略)
举个很常见的例子 [].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) => number
和 f3:() => number
, 则符合 函数兼容性, 可以赋值.
f2
,被传了两个参数,仅仅是用不到第二个参数而已,函数不会因此报错,对吧?
所以很安全,所以允许 变量sum:sumFunc
兼容 值f2
f1
因为 兼容性 的存在,只要是能安全执行,即使不符合 类型sumFunc
的定义,
TS认为 入参数量减少 是安全的,是兼容的,所以也给了通过,允许将值f2
赋值给 变量sum
这就是 为什么允许 入参数量减少 被兼容。
逆变也是类似的思维
TypeScript函数检查的逆变与协变原理
要理解 __逆变与协变__,
首先要明确,type约束,实际上是约束 变量 可以接收哪种类型的 值
其次要理解,赋值语句,当 被约束的变量 与 其被赋予的值 存在type差异时,TS将报错,
但在满足 兼容性 要求的情况下,一定范围内的type差异,是被允许而不报错的。
对于函数变量来说,存在 赋值 和 调用,两处类型检查。
赋值处检查,变量type 与 形参type 的兼容性
调用处检查,变量type 与 实参type 的兼容性
注意! 逆变与协变 是存在于 赋值处的 兼容性检查,
而要理解 逆变与协变 的合理性,则要站在 调用处 的类型检查角度 来感受。
而难以理解的点在于:
允许差异(兼容性)的范围判断标准是什么? 是否安全
对于函数type来说,这个兼容性规则是什么? 入参数量减少的值 & 符合逆变与协变规则的值
什么是逆变与协变?入参要求少能理解,为什么说逆变与协变也安全?
什么是逆变与协变?
入参类型的兼容(逆变) 返回值类型的兼容(协变)
在给限定了 函数类型的变量 赋值函数时
赋值给 变量的 函数
其参数 可以是 比变量类型定义里 要求的属性 更少(逆变)
返回值 可以是 比变量类型定义里 要求的属性 更多(协变)
简化版:
__给变量赋值的函数 参数可以属性更少,返回值可以属性更多__。
理解版:
用于赋值的函数的 参数 的属性 必须 比 被赋值的变量的要求 更少
用于赋值的函数的 返回值 的属性 必须 比 被赋值的变量的要求 更多
返回值的协变很好理解,这里主要讲逆变
为什么满足 逆变 会安全
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.理解逆变与协变.
逆变确实很难理解,之前写过一篇逆变与协变的文章,现在去看也有点蒙,遂写了一篇新的。
看到这里再回去看开头的总结,应该能体会到逆变的逻辑吧?
本文自己读了很多遍,且基于自己上一篇文章进行改动,已经尽量解释,尽量精简。
日常熬夜,感谢点赞,欢迎讨论。
寒冬已来,大家应该加紧 “囤货” 啊!