Typescript中的逆变与协变

news/2024/11/27 5:31:27/

许多不是很熟悉 TS 的朋友对于逆变和协变的概念会感到莫名的恐惧,没关系。它们仅仅代表阐述表现的概念而已,放心我们并不会从概念入手而是通过实例来逐步为你揭开它的面纱。

逆变 (函数入参)

首先,我们先来思考这样一个场景:

let a!: { a: string; b: number };
let b!: { a: string };b = a

我们都清楚 TS 属于静态类型检测,所谓类型的赋值是要保证安全性的。

通俗来说也就是多的可以赋值给少的,上述代码因为 a 的类型定义中完全包括 b 的类型定义,所以 a 类型完全是可以赋值给 b 类型,这被称为类型兼容性。

之后,我们再来思考这样一段代码:

let fn1!: (a: string, b: number) => void;
let fn2!: (a: string, b: number, c: boolean) => void;fn1 = fn2; // TS Error: 不能将fn2的类型赋值给fn1

我们将 fn2 赋值给 fn1 ,刚刚才提到类型兼容性的原因 TS 允许不同类型进行互相赋值(只需要父/子集关系),那么明明 fn2 的参数包括了所有的 fn1 为什么会报错?

上述的问题,其实和刚刚没有什么本质区别。我们来换一个角度来理解这个问题:

针对于 fn1 声明时,函数类型需要接受两个参数,换句话说调用 fn1 时我需要支持两个参数的传入分别是a:stringb:number

同理 fn2 函数定义时,定义了三个参数那么调用 fn2 时自然也需要传入三个参数。

那么此时,我们将 fn2 赋值给 fn1 ,我们可以思考下。如果赋值成功了,当我调用 fn1 时,其实相当于调用 fn2 没错吧。

但是,由于 fn1 的函数类型定义仅仅支持两个参数a:stringb:number即可。但是由于我们执行了fn1 = fn2

调用 fn1 时,实际相当于调用了 fn2 函数。但是类型定义上来说 fn1 满足两个参数传入即可,而 fn2 是实打实的需要传入 3 个参数。

那么此时,如果执行了fn1 = fn2当调用 fn1 时明显参数个数会不匹配(由于类型定义不一致)会缺少一个第三个参数,显然这是不安全的,自然也不是被 TS 允许的。

那么反过来呢?

let fn1!: (a: string, b: number) => void;
let fn2!: (a: string, b: number, c: boolean) => void;fn2 = fn1; // 正确,被允许

按照刚才的思路来分析,我们将 fn1 赋值给 fn2 。fn2 的类型定义需要支持三个参数的传入,但实际 fn2 内部指针已经被修改称为 fn1 的指针。

fn1 在执行时仅仅需要两个参数a: string, b: number,显然 fn2 的类型定义中是满足这个条件的(当然它还多传递了第三个参数c:boolean,在 JS 中对于函数而言调用时的参数个数大于定义时的参数个数是被允许的)。

自然,这是安全的也是被 TS 允许赋值。

就比如上述函数的参数类型赋值就被称为逆变,参数少(父)的可以赋给参数多(子)的那一个。看起来和类型兼容性(多的可以赋给少的)相反,但是通过调用的角度来考虑的话恰恰满足多的可以赋给少的兼容性原则。

上述这种函数之间互相赋值,他们的参数类型兼容性是典型的逆变。

我们再来看一个稍微复杂点的例子来加深所谓逆变的理解:

class Parent {}// Son继承了Parent 并且比parent多了一个实例属性 name
class Son extends Parent { public name: string = '19Qingfeng';
}// GrandSon继承了Son 在Son的基础上额外多了一个age属性
class Grandson extends Son { public age: number = 3;
}// 分别创建父子实例
const son = new Son();function someThing(cb: (param: Son) => any) { // do some someThing // 注意:这里调用函数的时候传入的实参是Son cb(Son);
}someThing((param: Grandson) => param); // error
someThing((param: Parent) => param); // correct

这里我们定义了三个类,他们之间的关系分别是 Parent 是基类,Son 继承 Parent ,Grandson 继承 Son 。

同时我们定义了一个函数,它接受一个 cb 回调参数作为参数,我们定义了这个回调函数的类型为接受一个 param 为 Son 实例类型的参数,此时我们不关心它的返回值给一个 any 即可。

注意这里,我们先用刚才的结论来推导。刚才我们提到过函数的参数的方式被称为逆变,所以当我们调用 someThing 时传递的 callback 需要赋给定义 something 函数中的 cb 。

换句话说类型(param: Grandson) => param需要赋给cb: (param: Son) => any,这显然是不被允许的。

因为逆变的效果函数的参数只允许“从少的赋值给多的”,显然 Grandson 相较于 Son 来说多了一个 name 属性少,所以这是不被允许的。

相反,第二个someThing((param: Parent) => param);相当于函数参数重将 Parent 赋给 Son 将少的赋给多的满足逆变,所以是正确的。

之后我们在尝试分析为什么第二个someThing((param: Parent) => param);是正确的。

首先我们需要注意到我们在定义 someThing 函数时,声明了这个函数接受一个 cb 的函数。这个函数接受一个类型为 Son 的参数。

someThing 内部cb 函数声明时需要满足 Son 的参数,它会在 cb 函数调用时传入一个 Son 参数的实参。

所以当我们传入someThing((param: Parent) => param)时,相当于在 something 函数内部调用(param: Parent) => param时会根据 someThing 中callback的定义传入一个 Son 。

那么此时,我们函数真实调用时期望得到是 Parent,但是实际得到了 Son 。Son 是 Parent 的子类涵盖所有 Parent 的公共属性方法,自然也是满足条件的。

反而言之,当我们使用someThing((param: Grandson) => param);,由于 something 定义 cb 的类型传入 Son,但是真实调用 someThing 时,我们确需要一个 Grandson 类型参数的函数,这显然是不符合的。

关于逆变我用了比较多的篇幅去描述它,我希望通过文章大家都可以对于逆变结合实例来理解并应用它。因为它的确稍微有些绕。

协变 (函数出参)

解决了逆变之后,其实协变对于大伙儿来说都是小意思。我们先来看看这个 Demo:

let fn1!: (a: string, b: number) => string;
let fn2!: (a: string, b: number) => string | number | boolean;fn2 = fn1; // correct 
fn1 = fn2 // error: 不可以将 string|number|boolean 赋给 string 类型

这里,函数类型赋值兼容时函数的返回值就是典型的协变场景,我们可以看到 fn1 函数返回值类型规定为 string,fn2 返回值类型规定为string | number | boolean

显然string | number | boolean是无法分配给 string 类型的,但是 string 类型是满足string | number | boolean其中之一,所以自然可以赋值给string | number | boolean组成的联合类型。

其实这就是协变…当然你也可以尝试从函数运行角度来解读协变的概念,比如当 fn1 运行结束要求返回 string , fn2 运行结束后要求返回string | number | boolean

将 fn1 赋给 fn2 ,fn1 要求返回值是 string ,而真实调用的fn1=fn2相当于调用了 fn2 自然string | number | boolean无法满足string类型的要求,所以 TS 会认为这是错误的。


http://www.ppmy.cn/news/1117151.html

相关文章

如何与QVC 建立EDI连接?

QVC,全称为Quality, Value, Convenience(品质、价值、便利),成立于1986年,是一家全球领先的零售电视和在线零售商。作为一家多渠道零售商,QVC致力于为客户提供高品质、独特的商品,通过电视、互联…

亚马逊云科技携手西门子运用生成式AI之力,打破数据孤岛

2023年,以基于GPT模型对话应用为代表的生成式AI浪潮席卷全球,引起企业广泛关注。自此,由生成式AI引导的企业变革序幕全面展开,企业向数智化转型迈出了坚实的一步。 西门子股份公司(以下简称“西门子”)是一…

无涯教程-JavaScript - SUMIFS函数

描述 SUMIFS函数添加其满足多个条件的所有参数。 语法 SUMIFS (sum_range, criteria_range1, criteria1, [criteria_range2, criteria2] ...)争论 Argument描述Required/OptionalSum_rangeThe range of cells to sum.RequiredCriteria_range1 使用Criteria1测试的范围。 Cr…

Linux上运行Nacos服务出现报错及解决方法

Linux上运行Nacos服务出现报错及解决方法 近期,有读者在运维家读者群中反馈,在Linux上运行Nacos服务时遇到了一个报错。以下是报错信息的描述: java.net.BindException: Address already in use: bind 这个报错信息表明Nacos服务器在尝试绑定…

开源版小剧场短剧影视小程序源码+支付收益等模式+付费短剧小程序源码+完整源码包和搭建教程

随着互联网的普及和技术的不断发展,越来越多的人开始接触和喜爱短剧影视作品。为了满足这一需求,市场上出现了许多短剧小程序。本文小编给大家分享一个开源版小剧场短剧影视小程序源码支付收益等模式,付费短剧小程序源码和完整源码包&#xf…

山石网科国产化防火墙,打造全方位边界安全解决方案

互联网的快速发展促进了各行各业的信息化建设,但也随之带来了诸多网络安全风险。大部分组织机构采用统一互联网接入方案,互联网出口承担着内部用户访问互联网的统一出口和对外信息服务的入口,因此在该区域部署相匹配的安全防护手段必不可少。…

指数杠杆平台是什么?融资杠杆一般是多少?

指数杠杆平台是近年来兴起的一种金融投资工具,它通过使用杠杆效应,允许投资者以较少的资金投入获得较大的投资回报。指数杠杆平台交易的产品通常是股票指数,例如道琼斯工业平均指数、纳斯达克综合指数等。 在指数杠杆平台交易中,…

分享一下微信公众号怎么实现积分商城功能

微信公众号作为一种社交媒体平台,可以帮助商家与消费者进行互动和沟通。除了实现微信拼团活动外,微信公众号还可以实现积分商城功能,提高消费者的参与度和忠诚度。本文将介绍如何在微信公众号实现积分商城功能。 一、了解积分商城 积分商城是…