跳到主要内容

类型变异

变异是类型系统中经常出现的一个话题。它用于确定类型参数在子类型化方面的行为方式。

首先,我们将设置几个相互扩展的类。

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 的值传递进来是否安全?” 如果是这种情况,那么 ST 的子类型。

使用我们的 CovariantOf 定义,并且鉴于 CityNoun 的子类型,CovariantOf<City> 也是 CovariantOf<Noun> 的子类型。确实

  • 当期望类型为 Noun 的属性时,从类型为 City 的属性 prop读取数据是安全的,并且
  • 当期望类型为 Noun 的值时,从调用 getter()返回类型为 City 的值是安全的。

将这两者结合起来,在需要 CovariantOf<Noun> 的任何地方使用 CovariantOf<City> 始终是安全的。

协变的一个常用示例是 $ReadOnlyArray<T>。就像 prop 属性一样,无法使用 $ReadOnlyArray 引用将数据写入数组。这允许更灵活的子类型化规则:Flow 只需要证明 ST 的子类型,才能确定 $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

NonCovariantOfCovariantOf 定义的区别在于,类型参数 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 可能返回一个可能不是 SanFranciscoCity

换句话说,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 写入是不安全的。