跳至主要内容

属性变异和其他即将到来的更改

Flow 的下一个版本 0.34 将包含一些对对象类型的重大更改

  • 属性变异,
  • 默认不变的字典类型,
  • 默认协变的方法类型,
  • 以及更灵活的 getter 和 setter。

什么是变异?

定义类型之间的子类型关系是 Flow 作为类型系统的核心职责。这些关系是直接为简单类型确定的,或者对于复杂类型,是根据它们的组成部分定义的。

变异描述了复杂类型的子类型关系,因为它与它们组成部分的子类型关系相关。

例如,Flow 直接编码了 string?string 的子类型的知识。直观地说,string 类型包含字符串值,而 ?string 类型包含 nullundefined 以及字符串值,因此前者的成员资格自然意味着后者的成员资格。

两个函数类型之间的子类型关系并不那么直接。相反,它是从函数的参数和返回值类型之间的子类型关系推导出来的。

让我们看看这对于两个简单的函数类型是如何工作的

type F1 = (x: P1) => R1;
type F2 = (x: P2) => R2;

F2 是否是 F1 的子类型取决于 P1P2 以及 R1R2 之间的关系。让我们使用符号 B <: A 来表示 BA 的子类型。

事实证明,如果 P1 <: P2R2 <: R1,则 F2 <: F1。请注意,参数的关系是相反的?从技术角度来说,我们可以说函数类型相对于它们的参数类型是“逆变的”,相对于它们的返回值类型是“协变的”。

让我们看一个例子

function f(callback: (x: string) => ?number): number {
return callback("hi") || 0;
}

我们可以将哪些类型的函数传递给 f?根据上面的子类型规则,我们可以传递一个参数类型是 string 的超类型,返回值类型是 ?number 的子类型的函数。

function g(x: ?string): number {
return x ? x.length : 0;
}
f(g);

f 的主体只会将 string 值传递给 g,这是安全的,因为 g 通过接受 ?string 至少接受 string。相反,g 只会将 number 值返回给 f,这是安全的,因为 f 通过处理 ?number 至少处理 number

输入和输出

记住某件事是协变还是逆变的一个方便方法是考虑“输入”和“输出”。

参数处于输入位置,通常称为“负”位置。复杂类型在其输入位置是逆变的。

返回是输出位置,通常称为“正”位置。复杂类型在其输出位置是协变的。

属性不变性

正如函数类型由参数类型和返回值类型组成一样,对象类型也由属性类型组成。因此,对象之间的子类型关系是从它们的属性的子类型关系推导出来的。

但是,与具有输入参数和输出返回值的函数不同,对象属性可以读取和写入。也就是说,属性既是输入又是输出。

让我们看看这对于两个简单的对象类型是如何工作的

type O1 = {p: T1};
type O2 = {p: T2};

与函数类型一样,O2 是否是 O1 的子类型取决于其组成部分 T1T2 之间的关系。

事实证明,如果 T2 <: T1T1 <: T2,则 O2 <: O1。从技术角度来说,对象类型相对于它们的属性类型是“不变的”。

让我们看一个例子

function f(o: {p: ?string}): void {
// We can read p from o
let len: number;
if (o.p) {
len = o.p.length;
} else {
len = 0;
}

// We can also write into p
o.p = null;
}

那么,我们可以将哪些类型的对象传递给 f?如果我们尝试传递一个具有子类型属性的对象,我们会得到一个错误

var o1: {p: string} = {p: ""};
f(o1);
function f(o: {p: ?string}) {}
^ null. This type is incompatible with
var o1: {p: string} = {p: ""};
^ string
function f(o: {p: ?string}) {}
^ undefined. This type is incompatible with
var o1: {p: string} = {p: ""};
^ string

Flow 正确地识别了这里的一个错误。如果 f 的主体将 null 写入 o.p,那么 o1.p 将不再具有 string 类型。

如果我们尝试传递一个具有超类型属性的对象,我们也会得到一个错误

var o2: {p: ?(string|number)} = {p: 0};
f(o2);
var o1: {p: ?(string|number)} = {p: ""};
^ number. This type is incompatible with
function f(o: {p: ?string}) {}
^ string

同样,Flow 正确地识别了一个错误,因为如果 f 尝试从 o 中读取 p,它会找到一个数字。

属性变异

因此,对象必须相对于它们的属性类型是不变的,因为属性可以从它们读取和写入。但仅仅因为你可以读取和写入,并不意味着你总是这样做。

考虑一个获取可空字符串属性长度的函数

function f(o: {p: ?string}): number {
return o.p ? o.p.length : 0;
}

我们从不写入 o.p,因此我们应该能够传递一个属性 p 的类型是 ?string 的子类型的对象。到目前为止,这在 Flow 中是不可能的。

使用属性变异,你可以显式地将对象属性注释为协变和逆变。例如,我们可以重写上面的函数

function f(o: {+p: ?string}): number {
return o.p ? o.p.length : 0;
}

var o: {p: string} = {p: ""};
f(o); // no type error!

至关重要的是,协变属性只能出现在输出位置。写入协变属性是一个错误

function f(o: {+p: ?string}) {
o.p = null;
}
o.p = null;
^ object type. Covariant property `p` incompatible with contravariant use in
o.p = null;
^ assignment of property `p`

相反,如果一个函数只写入一个属性,我们可以将该属性注释为逆变。例如,这可能出现在一个使用默认值初始化对象的函数中。

function g(o: {-p: string}): void {
o.p = "default";
}
var o: {p: ?string} = {p: null};
g(o);

逆变属性只能出现在输入位置。从逆变属性读取是一个错误

function f(o: {-p: string}) {
o.p.length;
}
o.p.length;
^ object type. Contravariant property `p` incompatible with covariant use in
o.p.length;
^ property `p`

默认不变的字典类型

对象类型 {[key: string]: ?number} 描述了一个可以用作映射的对象。我们可以读取任何属性,Flow 会推断结果类型为 ?number。我们还可以将 nullundefinednumber 写入任何属性。

在 Flow 0.33 及更早版本中,这些字典类型由类型系统协变地处理。例如,Flow 接受以下代码

function f(o: {[key: string]: ?number}) {
o.p = null;
}
declare var o: {p: number};
f(o);

这是不安全的,因为 f 可以用 null 覆盖属性 p。在 Flow 0.34 中,字典是不变的,就像命名属性一样。相同的代码现在会导致以下类型错误

function f(o: {[key: string]: ?number}) {}
^ null. This type is incompatible with
declare var o: {p: number};
^ number
function f(o: {[key: string]: ?number}) {}
^ undefined. This type is incompatible with
declare var o: {p: number};
^ number

协变和逆变字典可能非常有用。为了支持这一点,用于支持命名属性变异的相同语法也可以用于字典。

function f(o: {+[key: string]: ?number}) {}
declare var o: {p: number};
f(o); // no type error!

默认协变的方法类型

ES6 为我们提供了一种编写作为函数的对象属性的简写方式。

var o = {
m(x) {
return x * 2
}
}

Flow 现在将使用这种简写方法语法的属性解释为默认协变。这意味着写入属性 m 是一个错误。

如果你不想要协变,你可以使用长格式语法

var o = {
m: function(x) {
return x * 2;
}
}

更灵活的 getter 和 setter

在 Flow 0.33 及更早版本中,getter 和 setter 必须完全同意它们的返回值类型和参数类型,分别。Flow 0.34 取消了该限制。

这意味着你可以编写以下代码

// @flow
declare var x: string;

var o = {
get x(): string {
return x;
},
set x(value: ?string) {
x = value || "default";
}
}