在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 的一个子类型。
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);
}
这个函数同时接受User 和Admin 实例作为参数(以及你以后可能创建的User 的任何其他子类型的实例),变得更加可重用,并且不需要为细节而烦恼。
ts
logUsername(new User('user1')); // logs "user1"
logUsername(new Admin('admin1', true)); // logs "admin1"
1.2 一些帮助工具
现在让我们介绍一下符号A <: B --意思是*"A是B的一个子类型"。*因为Admin 是User 的一个子类型,现在你可以写得更短:
Admin <: User
让我们也定义一个辅助类型IsSubtypeOf<S, P> ,如果S 是P 的一个子类型,它的值为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 的子类型,或者字面数字类型42 是number 的子类型。
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.共变性
让我们想想一些异步代码,它获取User 和Admin 实例。因此,你必须与User 和Admin 的承诺一起工作。
有了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 类型是协变的。
这里有一个共变的定义。
一个类型
T是共变的,如果有S <: P,那么T<S> <: T<P>。
一个类型的协变性是直观的。如果Admin 是User 的一个子类型,那么你可以期望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 类型的这种行为使其成为反变量。换句话说,函数类型对于它们的参数类型来说是禁变的。
一个类型
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模式时。
换句话说,函数的子类型要求参数类型是禁变量,而返回类型是协变量。
比如说
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 是禁变的。该函数类型通过参数类型是不变的,但通过返回类型是不变的。
挑战:你还知道哪些协变或禁变的类型?