类型|04 TypeScript中的逆变与协变

类型|04 TypeScript中的逆变与协变
type
status
date
slug
summary
tags
category
icon
password
📌
本文出自《前端技术全方位深度进阶指南》系列教程-基础篇
在 TypeScript 中,类型系统的一部分是关于如何处理函数参数和返回值的类型的。这就涉及到了 逆变(contravariant) 和 协变(covariant) 的概念。在TypeScript中学习协变性和逆变性可能有些棘手,但是了解它们对于理解类型和子类型是一个很好的补充。

01 子类型化

子类型化是多态性的一种形式,其中 子类型 通过某种形式的可替换性与 基本类型 相关联。 可替换性意味着基本类型的变量也可以接受子类型值。
例如,我们定义一个基类 User 和一个 Admin(扩展 User 类)的类:
 
由于Admin扩展自 User(Admin extends User),我们可以说Admin是基本类型User的子类型。
notion image
Admin(子类型)和User(基本类型)的可替换性在于可以将Admin类型的实例分配给User类型的变量,例如:
如何从可替代性中受益?
最大的好处之一是可以定义不依赖于细节的行为。简而言之,您可以创建接受基类型作为参数的函数,然后可以使用子类型调用该函数。
例如,编写一个将用户名记录到控制台的函数:
该函数接受的参数类型可以为UserAdmin和任何其他基于User子类型。这使得logUsername()更具可重用性,并且不用关注细节。

😆 工具函数

现在让我们介绍一下这个符号 A <: B —— 意思是 “A是B的子类型”。
因为 Admin 是 User 的子类型,现在你可以简写成:
我们定义一个辅助类型 IsSubtypeOf<S, P>,如果 S 是 P 的子类型,则评估为 true,否则为 false
IsSubtypeOf<Admin, User> 计算结果为true 因为AdminUser的子类型:
很多类型都可以进行子类型化,包括基础类型和内置 JavaScript 类型。
例如字面量字符串'Hello'的类型是 string 的子类型,字面量数字 42 的类型是 number 的子类型,Map<K, V>Object的子类型。

02 协变(Covariant)

假设我们有一段异步代码用于获取 User 和 Admin 实例,需要处理 Promise<User> 和 Promise<Admin>
一个有趣的问题是:如果 Admin <: User,那么 Promise<Admin> <: Promise<User> 是否也成立?这里做个实验:
当 Admin <: User 时,Promise<Admin> <: Promise<User> 也成立。这说明Promise是 协变(Covariant)类型
notion image

😁 协变(Covariant)的定义:

如果 S <: P 且 T<S> <: T<P> 那就可以说 T 是 协变类型
如果Admin是User的子类型,那么就可以预期 Promise<Admin> 是 Promise<User> 的子类型。
协变在TypeScript中适用于许多类型
  • A) Promise<V>(如上所示)
  • B) Record<K,V>:
  • C) Map<K,V>:

03 逆变(Contravariant)

分析以下泛型类型:
Func<Param>创建了一个函数类型,该类型带一个类型为Param的参数。
Admin <: User时,以下哪个表达式为真:
  • Func<Admin> <: Func<User> 或者
  • Func<User> <: Func<Admin> ?
现在试一下:
Func<User> <: Func<Admin>成立 - 这意味着Func<User>Func<Admin>的子类型。与原始类型关系 Admin <: User相比,子类型的方向已经发生 翻转
Func 类型的这种行为使其具有 逆变性(Contravariant) 。一般来说,函数类型相对其参数类型是逆变的。
notion image

😎 逆变(Contravariant)的定义:

如果 S <: P 且 T<P> <: T<S> 那就可以说 T 是逆变类型
函数类型的子类型化方向与参数类型的子类型化方向相反。

04 函数子类型化

函数子类型化结合了协变和逆变。
如果一个函数的参数类型相对于其基本类型是逆变的(Contravariant),并且返回类型相对于其基本类型的返回类型是协变的(Covariant),那么该函数类型是其基本类型的子类型。
注:当启用 strictFunctionTypes 模式时。
换句话说,函数的子类型化要求参数类型是逆变的,而返回类型是协变的。
notion image
例如:
SubtypeFunc <: BaseFunc 因为:
  • A) 参数类型是逆变的(子类型化方向翻转 User :> Admin
  • B) 返回类型是协变的(相同的子类型方向 '1' | '2' <: string)。
了解子类型化非常有助于理解函数类型的可替换性。
例如,有一个Admin实例列表:
接受什么类型的回调 admins.filter(...) 呢?显然,它接受带有一个 Admin 类型参数的回调:
但 admins.filter(...) 是否能接受 User 类型参数的回调呢?
没错 admins.filter() 接受 (admin: Admin) => boolean 基本类型,同时也接受其子类型,例如 (user: User) => boolean
如果高阶函数接受特定类型的回调,例如 (admin: Admin) => boolean,那么还可以提供作为特定类型的子类型的回调,例如 (user: User) => boolean

05 结论

假设有两个类型 S 与 P 两者关系 S <: P
1、如果 T<S> <: T<P> (子类型方向被保持),则类型 T 是**协变的。 如 Promise<T> 即是一个协变类型。
2、如果 T<P> <: T<S> (子类型关系被翻转),则类型 T 是逆变的。
函数子类型化在参数类型上是逆变的,但在返回类型上是协变的。
上一篇
01|macOS上可直接载文的跟打器|木易跟打器|源来
下一篇
技巧|03 JavaScript 中将ArrayBuffer 转换为字符串
Loading...