Flow 的下一个版本 0.34 将包含一些对对象类型的重大更改
- 属性变异,
- 默认不变的字典类型,
- 默认协变的方法类型,
- 以及更灵活的 getter 和 setter。
什么是变异?
定义类型之间的子类型关系是 Flow 作为类型系统的核心职责。这些关系是直接为简单类型确定的,或者对于复杂类型,是根据它们的组成部分定义的。
变异描述了复杂类型的子类型关系,因为它与它们组成部分的子类型关系相关。
例如,Flow 直接编码了 string
是 ?string
的子类型的知识。直观地说,string
类型包含字符串值,而 ?string
类型包含 null
、undefined
以及字符串值,因此前者的成员资格自然意味着后者的成员资格。
两个函数类型之间的子类型关系并不那么直接。相反,它是从函数的参数和返回值类型之间的子类型关系推导出来的。
让我们看看这对于两个简单的函数类型是如何工作的
type F1 = (x: P1) => R1;
type F2 = (x: P2) => R2;
F2
是否是 F1
的子类型取决于 P1
和 P2
以及 R1
和 R2
之间的关系。让我们使用符号 B <: A
来表示 B
是 A
的子类型。
事实证明,如果 P1 <: P2
且 R2 <: 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
的子类型取决于其组成部分 T1
和 T2
之间的关系。
事实证明,如果 T2 <: T1
且T1 <: 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
。我们还可以将 null
或 undefined
或 number
写入任何属性。
在 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";
}
}