在 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的子类型”。
因为 Admin
是 User
的子类型,现在你可以简写成:
Admin <: User
我们定义一个辅助类型 IsSubtypeOf<S, P>
,如果 S
是 P
的子类型,则评估为 true
,否则为 false
:
type IsSubtypeOf<S, P> = S extends P ? true : false;
IsSubtypeOf<Admin, User>
计算结果为true 因为Admin
是User
的子类型:
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)
假设我们有一段异步代码用于获取 User
和 Admin
实例,需要处理 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 <: P
且T<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) 。一般来说,函数类型相对其参数类型是逆变的。