TypeScript中的协方差和反方差介绍

334 阅读5分钟

在TypeScript中学习协方差和反方差可能很棘手(我知道我的经验!),但了解它们是对理解类型和子类型的一个很好补充。

在这篇文章中,你将读到关于协变和反变概念的易懂的解释。

目录

1.子类型化

子类型化是多态性的一种形式,在这种形式中,类型的数据类型与超类型的数据类型相关联,超类型的数据类型也被称为基类型,通过某种形式的可替代性。

可替代性意味着基类型的每个变量、函数参数也可以接受子类型的值。

例如,让我们定义一个基类User ,然后用Admin 类来扩展它。

ts

class User {
  username: string;
 
  constructor(username: string) {
    this.username = username;
  }
}
 
class Admin extends User {
  isSuperAdmin: boolean;
 
  constructor(username: string, isSuperAdmin: boolean) {
    super(username);
    this.isSuperAdmin = isSuperAdmin;
  }
}

由于Admin 扩展了User (Admin extends User),你可以说Admin基类型 User 的一个类型。

User and Admin Classes

1.1 为什么要理解子类型?

Admin (子类型)和User (基类型)的可替代性包括,例如,能够将类型User 的变量分配给类型Admin 的实例。

ts

const user1: User = new User('user1');         // OK
const user2: User = new Admin('admin1', true); // also OK

这是一个很好的技巧。但为什么这很重要呢?你如何从理解子类型和可替换性中获益?

在众多好处中,我们可以创建接受一个基本类型的函数,同时也接受所有的子类型。

例如,让我们写一个将用户名记录到控制台的函数。

ts

function logUsername(user: User): void {
  console.log(user.username);
}

这个函数同时接受UserAdmin 实例作为参数(以及你以后可能创建的User 的任何其他子类型的实例),变得更加可重用,并且不需要为细节而烦恼。

ts

logUsername(new User('user1'));         // logs "user1"
logUsername(new Admin('admin1', true)); // logs "admin1"

1.2 一些帮助工具

现在让我们介绍一下符号A <: B --意思是*"A是B的一个子类型"。*因为AdminUser 的一个子类型,现在你可以写得更短:

Admin <: User

让我们也定义一个辅助类型IsSubtypeOf<S, P> ,如果SP 的一个子类型,它的值为true ,否则为false

ts

type IsSubtypeOf<S, P> = S extends P ? true : false;

IsSubtypeOf<Admin, User> 评估为 ,因为 是 的一个子类型。true Admin User

ts

type T11 = IsSubtypeOf<Admin, User>;
     
type T11 = true

顺便提一下,其他类型也可以进行子类型化。例如,字面字符串类型'Hello'string 的子类型,或者字面数字类型42number 的子类型。

ts

type T12 = IsSubtypeOf<'hello', string>;
     
type T12 = true
type T13 = IsSubtypeOf<42, number>;
     
type T13 = true
type T14 = IsSubtypeOf<Map<string, string>, Object>
     
type T14 = true

2.共变性

让我们想想一些异步代码,它获取UserAdmin 实例。因此,你必须与UserAdmin 的承诺一起工作。

有了Admin <: User ,是否意味着Promise<Admin> <: Promise<User> 也是成立的?换句话说,Promise<Admin>Promise<User> 的一个子类型吗?

让我们看看TypeScript是怎么说的。

ts

type T21 = IsSubtypeOf<Promise<Admin>, Promise<User>>
     
type T21 = true

事实上,Promise<Admin> <: Promise<User> 是成立的,有Admin <: User 。说得正式一点,Promise 类型是协变的

Covariance

这里有一个共变定义。

一个类型T共变的,如果有S <: P ,那么T<S> <: T<P>

一个类型的协变性是直观的。如果AdminUser 的一个子类型,那么你可以期望Promise<Admin>Promise<User> 的一个子类型。

共变性在TypeScript中对许多类型都是成立的。

A)Promise<V> (上面展示的)

B)Record<K,V> :

ts

type RecordOfAdmin = Record<string, Admin>;
type RecordOfUser  = Record<string, User>;
 
type T22 = IsSubtypeOf<RecordOfAdmin, RecordOfUser>;
     
type T22 = true

C)Map<K,V> :

ts

type MapOfAdmin = Map<string, Admin>;
type MapOfUser  = Map<string, User>;
 
type T23 = IsSubtypeOf<MapOfAdmin, MapOfUser>;
     
type T23 = true

3.共变性

现在让我们来考虑以下的通用类型。

ts

type Func<Param> = (param: Param) => void;

Func<Param> 创建有一个参数类型的函数类型 Param

拥有Admin <: User ,哪个表达式是真的:Func<Admin> <: Func<User> ,或Func<User> <: Func<Admin>

Func<Admin>Func<User> 的一个子类型,或者反过来说:Func<User>Func<Admin> 的一个子类型?

让我们试一试。

ts

type T31 = IsSubtypeOf<Func<Admin>, Func<User>>
     
type T31 = false
type T32 = IsSubtypeOf<Func<User>, Func<Admin>>
     
type T32 = true

如上面的例子所示,Func<Admin> <: Func<User> 是假的。

相反,Func<User> <: Func<Admin> 为真--意味着Func<User>Func<Admin> 的一个子类型。与原始类型Admin <: User 相比,子类型的方向发生了翻转。

Func 类型的这种行为使其成为反变量。换句话说,函数类型对于它们的参数类型来说是禁变的。

Contravariance

一个类型T ,如果有S <: P ,那么T<P> <: T<S> ,就是禁变的

简单地说,函数类型的子类型与参数类型的子类型决定了相反的方向(当返回类型相同时)。

ts

type FuncUser = (p: User) => void;
type FuncAdmin = (p: Admin) => void;
 
type T31 = IsSubtypeOf<Admin, User>;
     
type T31 = true
 
type T32 = IsSubtypeOf<FuncUser, FuncAdmin>;
     
type T32 = true

4.函数子类型化

关于函数类型子类型化的有趣之处在于,它结合了变异和禁忌。

如果一个函数类型的参数类型与基类型的参数类型是互斥的,并且返回类型与基类型的返回类型是共变的*,那么这个函数类型就是基类型的一个子类型

*当启用strictFunctionTypes模式时

换句话说,函数的子类型要求参数类型是禁变量,而返回类型是协变量。

Function Types Subtyping in TypeScript

比如说

ts

type SubtypeFunc = (p: User) => '1' | '2';
type BaseFunc = (p: Admin) => string;  
 
type T41 = IsSubtypeOf<SubtypeFunc, BaseFunc>
     
type T41 = true

SubtypeFunc <: BaseFunc 因为。

A)参数类型是禁忌的(子类型方向翻转User :> Admin
B)返回类型是共变的(同一子类型方向'1' | '2' <: string )。

了解subtyping大大有助于理解函数类型的可替代性。

例如,有一个Admin 实例的列表。

ts

const admins: Admin[] = [
  new Admin('john.smith', false),
  new Admin('jane.doe', true),
  new Admin('joker', false)
];

admins.filter(...) 接受什么类型的回调?

很明显,它接受一个有一个参数类型的回调,Admin

ts

const admins: Admin[] = [
  new Admin('john.smith', false),
  new Admin('jane.doe', true),
  new Admin('joker', false)
];
 
const superAdmins = admins.filter((admin: Admin): boolean => {
  return admin.isSuperAdmin;
});
 
superAdmins; // [ Admin('jane.doe', true) ]

但是admins.filter(...) 会接受一个参数类型为User 的回调吗?

const jokers = admins.filter((user: User): boolean => {
  return user.username.startsWith('joker');
});
 
jokers; // [ Admin('joker', false) ]

是的,admins.filter() 接受(admin: Admin) => boolean 基本类型,但也接受它的子类型,如(user: User) => boolean

如果一个高阶函数接受特定类型的回调,例如(admin: Admin) => boolean ,那么你也可以提供特定类型的子类型的回调,例如(user: User) => boolean

5.总结

如果有2个类型S <: P ,那么T<S> <: T<P> (子类型方向保持不变),那么T 这个类型就是共变的。共变类型的一个例子是Promise<T>

但是如果T<P> <: T<S> (子类型化被翻转),那么T 是禁变的。该函数类型通过参数类型是不变的,但通过返回类型是不变的。

挑战:你还知道哪些协变或禁变的类型?