类型变异
变异是类型系统中经常出现的一个话题。它用于确定类型参数在子类型化方面的行为方式。
首先,我们将设置几个相互扩展的类。
class Noun {}
class City extends Noun {}
class SanFrancisco extends City {}
我们在关于 泛型类型 的部分中看到,可以使用变异符号来描述类型参数何时在输出位置使用,何时在输入位置使用,以及何时在两者中使用。
在这里,我们将深入探讨这三种情况中的每一种。
协变
例如,考虑类型
type CovariantOf<X> = {
+prop: X;
getter(): X;
}
这里,X
严格地出现在输出位置:它用于从类型为 CovariantOf<X>
的对象 o
中读取信息,无论是通过属性访问 o.prop
,还是通过调用 o.getter()
。
值得注意的是,由于 prop
是一个只读属性,因此无法通过对对象 o
的引用输入数据。
当这些条件成立时,我们可以使用符号 +
来注释 CovariantOf
定义中的 X
type CovariantOf<+X> = {
+prop: X;
getter(): X;
}
这些条件对我们如何处理类型为 CovariantOf<T>
的对象以及子类型化方式有重要影响。提醒一下,子类型化规则帮助我们回答这个问题:“给定一些期望类型为 T
的值的上下文,将类型为 S
的值传递进来是否安全?” 如果是这种情况,那么 S
是 T
的子类型。
使用我们的 CovariantOf
定义,并且鉴于 City
是 Noun
的子类型,CovariantOf<City>
也是 CovariantOf<Noun>
的子类型。确实
- 当期望类型为
Noun
的属性时,从类型为City
的属性prop
中读取数据是安全的,并且 - 当期望类型为
Noun
的值时,从调用getter()
中返回类型为City
的值是安全的。
将这两者结合起来,在需要 CovariantOf<Noun>
的任何地方使用 CovariantOf<City>
始终是安全的。
协变的一个常用示例是 $ReadOnlyArray<T>
。就像 prop
属性一样,无法使用 $ReadOnlyArray
引用将数据写入数组。这允许更灵活的子类型化规则:Flow 只需要证明 S
是 T
的子类型,才能确定 $ReadOnlyArray<S>
也是 $ReadOnlyArray<T>
的子类型。
不变性
让我们看看如果我们尝试放宽对 X
的使用限制,并使 prop
成为一个可读写的属性,会发生什么。我们得到类型定义
type NonCovariantOf<X> = {
prop: X;
getter(): X;
};
让我们还声明一个类型为 NonCovariantOf<City>
的变量 nonCovariantCity
declare const nonCovariantCity: NonCovariantOf<City>;
现在,将 nonCovariantCity
视为类型为 NonCovariantOf<Noun>
的对象是不安全的。如果我们被允许这样做,我们就可以有以下声明
const nonCovariantNoun: NonCovariantOf<Noun> = nonCovariantCity;
这种类型允许以下赋值
nonCovariantNoun.prop = new Noun;
这将使 nonCovariantCity
的原始类型失效,因为它现在将在其 prop
字段中存储一个 Noun
。
NonCovariantOf
与 CovariantOf
定义的区别在于,类型参数 X
在输入和输出位置都使用,因为它用于读写属性 prop
。这种类型参数称为不变,是变异的默认情况,因此不需要添加前缀符号
type InvariantOf<X> = {
prop: X;
getter(): X;
setter(X): void;
};
假设一个变量
declare const invariantCity: InvariantOf<City>;
在需要
InvariantOf<Noun>
的上下文中使用invariantCity
是不安全的,因为我们不应该能够将Noun
写入属性prop
。InvariantOf<SanFrancisco>
的上下文中使用invariantCity
是不安全的,因为读取prop
可能返回一个可能不是SanFrancisco
的City
。
换句话说,InvariantOf<City>
既不是 InvariantOf<Noun>
的子类型,也不是 InvariantOf<SanFrancisco>
的子类型。
逆变
当类型参数仅在输入位置使用时,我们说它以逆变方式使用。这意味着它只出现在我们向结构写入数据的那些位置。我们使用符号 -
来描述这种类型的参数
type ContravariantOf<-X> = {
-prop: X;
setter(X): void;
};
常见的逆变位置是只写属性和“setter”函数。
类型为 ContravariantOf<City>
的对象可以在需要类型为 ContravariantOf<SanFrancisco>
的对象的任何地方使用,但不能在需要 ContravariantOf<Noun>
的地方使用。换句话说,ContravariantOf<City>
是 ContravariantOf<SanFrancisco>
的子类型,但不是 ContravariantOf<Noun>
的子类型。这是因为将 SanFrancisco
写入可以写入任何 City
的属性是安全的,但将任何 Noun
写入是不安全的。