Typescript中的逆变与协变

这篇具有很好参考价值的文章主要介绍了Typescript中的逆变与协变。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

许多不是很熟悉 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 会认为这是错误的。文章来源地址https://www.toymoban.com/news/detail-730623.html

到了这里,关于Typescript中的逆变与协变的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 深入理解Java泛型、协变逆变、泛型通配符、自限定

    重写了之前博客写的泛型相关内容,全部整合到这一篇文章里了,把坑都填了,后续不再纠结这些问题了。本文深度总结了函数式思想、泛型对在Java中的应用,解答了许多比较难的问题。 纯函数 协变 逆变 泛型通配符 PECS法则 自限定 Java8 引入了函数式接口,从此方法传参可

    2023年04月20日
    浏览(52)
  • 【TypeScript】TypeScript中的泛型

    定义一个函数或类时,有些情况下无法确定其中要使用的具体类型(返回值、参数、属性的类型不能确定),此时泛型便能够发挥作用。 举个例子: 上例中,test函数有一个参数类型不确定,但是能确定的时其返回值的类型和参数的类型是相同的,由于类型不确定所以参数和

    2024年02月09日
    浏览(46)
  • 【JavaSE】Java进阶知识一(泛型详解,包括泛型方法,协变,逆变,擦除机制)

    目录 泛型 1. 什么是泛型 2.泛型方法 3.通配符上界(泛型的协变) 4.通配符下界(泛型的逆变) 5.泛型的编译(擦除机制)         泛型:就是让一个类能适用于多个类型,就是在封装数据结构时能让封装的类型被各种类型使用所以引入了泛型的概念,虽然有了泛型,什么数

    2024年02月04日
    浏览(47)
  • TypeScript教程(一)在vscode中的配置TypeScript环境

    未来的开发者们请上座,随着时代的发展web网页的发展越来越丰富,掌握TypeScript显得更加有必要。 安装Node.js:首先,确保您已经安装了Node.js。您可以从Node.js官方网站(https://nodejs.org)下载并安装适合您操作系统的版本。 npm 切换淘宝镜像源 查询镜像使用状态 npm 安装TypeS

    2024年01月24日
    浏览(49)
  • typeScript中的函数类型

    目录 1.函数声明 2.函数表达式 3.用接口定义函数的形状 4.可选参数 5.参数默认值  6.剩余参数 7.重载 函数是JavaScript应用程序的基础。它帮助你实现抽象层, 模拟类,信息隐藏和模块。在TypeScript里, 虽然已经支持类,命名空间和模块,但函数仍然是主要的定义行为的地方。

    2024年02月03日
    浏览(38)
  • TypeScript 中的常用类型声明大全

    上一章节,我们介绍了什么是TS,以及TS的应用场景。本章节将给大家介绍,在TypeScript 中,常用的数据类型声明,有我们熟悉的 基本数据类型,也有,一些TypeScript 新增的一些,语法规范类型。出发吧… 函数:同样的,也可以给函数限制 接受形参的类型和,限制返回值的数

    2024年02月16日
    浏览(37)
  • TypeScript中的类型声明declare

    在 TypeScript 中, declare 用于定义 全局 变量、函数和类型等 ,提供了一种在编译过程中告诉TypeScript编译器某个标识符的类型的方式。它告诉编译器:虽然它在当前文件中没有声明,但它在其他地方已经存在了。也就是说,declare让 编译器 知道 这些声明的实体是在编译

    2024年02月12日
    浏览(47)
  • 认识TypeScript 中的接口和类

         🎬 江城开朗的豌豆 :个人主页  🔥 个人专栏  :《 VUE 》 《 javaScript 》  📝  个人网站  :《 江城开朗的豌豆🫛 》  ⛺️ 生活的理想,就是为了理想的生活 ! 目录 接口 类 江城开朗的豌豆 在 TypeScript 中,接口(Interfaces)和类(Classes)是实现面向对象编程(O

    2024年02月21日
    浏览(32)
  • TypeScript 中的类型检查实用函数

    在前端开发中,我们经常需要判断变量的类型以进行相应的操作或处理。TypeScript 提供了基础的类型检查,但有时我们需要更复杂或更灵活的类型检查。这篇博客文章将介绍一组实用函数,用于各种常见的类型检查。

    2024年02月10日
    浏览(41)
  • TypeScript中的模块与命名空间

    在 TypeScript 中, 模块 是一种组织和封装代码的方式。模块使得代码可以按照特定的规则划分为不同的文件,并且可以在这些文件之间进行导入和导出,从而实现代码的重用和组织。 默认模块 导出是一种特殊的导出语法,在一个模块中只能有一个默认导出。默认导出可以是任

    2024年02月12日
    浏览(33)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包