跳到主要内容

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

· 阅读需 9 分钟
木易(OwenYang)

TypeScript 中,类型系统的一部分是关于如何处理函数参数和返回值的类型的。这就涉及到了 逆变(contravariant)协变(covariant) 的概念。在TypeScript中学习协变性和逆变性可能有些棘手,但是了解它们对于理解类型和子类型是一个很好的补充。

01 子类型化

子类型化是多态性的一种形式,其中 子类型 通过某种形式的可替换性与 基本类型 相关联。 可替换性意味着基本类型的变量也可以接受子类型值。

例如,我们定义一个基类 User 和一个 Admin(扩展 User 类)的类:

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的子类型。

Admin(子类型)和User(基本类型)的可替换性在于可以将Admin类型的实例分配给User类型的变量,例如:

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

如何从可替代性中受益?

最大的好处之一是可以定义不依赖于细节的行为。简而言之,您可以创建接受基类型作为参数的函数,然后可以使用子类型调用该函数。

例如,编写一个将用户名记录到控制台的函数:

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

该函数接受的参数类型可以为User,Admin和任何其他基于User子类型。这使得logUsername()更具可重用性,并且不用关注细节。

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

😆 工具函数

现在让我们介绍一下这个符号 A <: B —— 意思是 “A是B的子类型”

因为 AdminUser 的子类型,现在你可以简写成:

Admin <: User

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

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

IsSubtypeOf<Admin, User> 计算结果为true 因为AdminUser的子类型:

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

很多类型都可以进行子类型化,包括基础类型和内置 JavaScript 类型。

例如字面量字符串'Hello'的类型是 string 的子类型,字面量数字 42 的类型是 number 的子类型,Map<K, V>Object的子类型。

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

02 协变(Covariant)

假设我们有一段异步代码用于获取 UserAdmin 实例,需要处理 Promise<User>Promise<Admin>

一个有趣的问题是:如果 Admin <: User,那么 Promise<Admin> <: Promise<User> 是否也成立?这里做个实验:

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

Admin <: User 时,Promise<Admin> <: Promise<User> 也成立。这说明Promise是 协变(Covariant)类型

😁 协变(Covariant)的定义:

如果 S <: PT<S> <: T<P> 那就可以说 T协变类型

如果Admin是User的子类型,那么就可以预期 Promise<Admin>Promise<User> 的子类型。

协变在TypeScript中适用于许多类型

  • A) Promise<V>(如上所示)
  • B) Record<K,V>:
type RecordOfAdmin = Record<string, Admin>;
type RecordOfUser = Record<string, User>;

type T22 = IsSubtypeOf<RecordOfAdmin, RecordOfUser>; // true
  • C) Map<K,V>:
type MapOfAdmin = Map<string, Admin>;
type MapOfUser = Map<string, User>;

type T23 = IsSubtypeOf<MapOfAdmin, MapOfUser>; // true

03 逆变(Contravariant)

分析以下泛型类型:

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

Func<Param>创建了一个函数类型,该类型带一个类型为Param的参数。

Admin <: User时,以下哪个表达式为真:

  • Func<Admin> <: Func<User> 或者
  • Func<User> <: Func<Admin>

现在试一下:

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

Func<User> <: Func<Admin>成立 - 这意味着Func<User>Func<Admin>的子类型。与原始类型关系 Admin <: User相比,子类型的方向已经发生 翻转

Func 类型的这种行为使其具有 逆变性(Contravariant) 。一般来说,函数类型相对其参数类型是逆变的。

😎 逆变(Contravariant)的定义:

如果 S <: PT<P> <: T<S> 那就可以说 T逆变类型

函数类型的子类型化方向与参数类型的子类型化方向相反。

type FuncUser = (p: User) => void;
type FuncAdmin = (p: Admin) => void;

type T31 = IsSubtypeOf<Admin, User>; // true

type T32 = IsSubtypeOf<FuncUser, FuncAdmin>; // true

04 函数子类型化

函数子类型化结合了协变和逆变。

如果一个函数的参数类型相对于其基本类型是逆变的(Contravariant),并且返回类型相对于其基本类型的返回类型是协变的(Covariant),那么该函数类型是其基本类型的子类型。

注:当启用 strictFunctionTypes 模式时。

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

例如:

type SubtypeFunc = (p: User) => '1' | '2';
type BaseFunc = (p: Admin) => string;

type T41 = IsSubtypeOf<SubtypeFunc, BaseFunc> // true

SubtypeFunc <: BaseFunc 因为:

  • A) 参数类型是逆变的(子类型化方向翻转 User :> Admin
  • B) 返回类型是协变的(相同的子类型方向 '1' | '2' <: string)。

了解子类型化非常有助于理解函数类型的可替换性。

例如,有一个Admin实例列表:

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

接受什么类型的回调 admins.filter(...) 呢?显然,它接受带有一个 Admin 类型参数的回调:

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;
});

console.log(superAdmins); // [ Admin('jane.doe', true) ]

admins.filter(...) 是否能接受 User 类型参数的回调呢?

const jokers = admins.filter((user: User): boolean => {
return user.username.startsWith('joker');
});

console.log(jokers); // [ Admin('joker', false) ]

没错 admins.filter() 接受 (admin: Admin) => boolean 基本类型,同时也接受其子类型,例如 (user: User) => boolean

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

05 结论

假设有两个类型 SP 两者关系 S <: P

1、如果 T<S> <: T<P> (子类型方向被保持),则类型 T 是**协变的。 Promise<T> 即是一个协变类型。

2、如果 T<P> <: T<S> (子类型关系被翻转),则类型 T逆变的。

函数子类型化在参数类型上是逆变的,但在返回类型上是协变的。