注释要求
注意: 从 0.199 版本开始,Flow 使用 局部类型推断 作为其推断算法。本节中的规则反映了此推断方案中的主要设计特征。
Flow 试图避免在程序的某些部分要求类型注释,在这些部分,类型可以很容易地从表达式、变量、参数等的直接上下文中推断出来。
变量声明
以以下变量定义为例
const len = "abc".length;
推断 len
类型所需的所有信息都包含在初始化器 "abc".length
中。Flow 将首先确定 "abc"
是一个字符串,然后确定字符串的 length
属性是一个数字。
相同的逻辑可以应用于所有 const
类型的初始化。当变量初始化跨越多个语句时,情况会变得更加复杂,例如在
1declare const maybeString: ?string;2
3let len;4if (typeof maybeString === "string") {5 len = maybeString.length;6} else {7 len = 0;8}
Flow 仍然可以确定 len
是一个 number
,但为了做到这一点,它会向前查看多个初始化语句。有关各种初始化模式如何确定变量类型以及何时需要在变量声明上添加注释的详细信息,请参阅有关 变量声明 的部分。
函数参数
与变量声明不同,这种“向前看”推理不能用于确定函数参数的类型。考虑以下函数
function getLength(x) {
return x.length;
}
我们可以访问并返回 length
属性的 x
有很多种类型:具有 length
属性的对象,或者字符串,仅举几例。如果稍后在程序中我们对 getLength
进行以下调用
getLength("abc");
getLength({length: 1});
一种可能的推断是 x
是一个 string | { length: number }
。然而,这意味着 getLength
的类型由当前程序的任何部分决定。这种全局推理会导致令人惊讶的“远距离作用”行为,因此应避免。相反,Flow 要求对函数参数进行注释。如果未提供此类类型注释,则会在参数 x
上显示 [missing-local-annot]
错误,并且函数主体将使用 x: any
进行检查
1function getLength(x) { 2 return x.length;3}4
5const n = getLength(1); // no error since getLength's parameter type is 'any'
1:20-1:20: Missing an annotation on `x`. [missing-local-annot]
要修复此错误,可以简单地将 x
注释为
1function getLength(x: string) {2 return x.length;3}
类方法也具有相同的要求
1class WrappedString {2 data: string;3 setStringNoAnnotation(x) { 4 this.data = x;5 }6 setString(x: string) {7 this.data = x;8 }9}
3:25-3:25: Missing an annotation on `x`. [missing-local-annot]
上下文类型
函数参数并不总是需要显式注释。在函数调用回调函数的情况下,参数类型可以很容易地从直接上下文中推断出来。例如,考虑以下代码
const arr = [0, 1, 2];
const arrPlusOne = arr.find(x => x % 2 === 1);
Flow 推断 arr
的类型为 Array<number>
。将此与 Array.find
的内置信息结合起来,Flow 可以确定 x => x % 2 + 1
的类型需要为 number => mixed
。此类型充当 Flow 的提示,并提供足够的信息来确定 x
的类型为 number
。
任何伴随注释都可能充当函数参数提示,例如
1const fn1: (x: number) => number = x => x + 1;
但是,注释也可能无法用作函数参数提示
1const fn2: mixed = x => x + 1;
1:20-1:20: An annotation on `x` is required because Flow cannot infer its type from local context. [missing-local-annot]
在此示例中,mixed
类型 simply does not include enough information to extract a candidate type for x
.
即使函数参数嵌套在其他表达式(如对象)中,Flow 也可以推断未注释参数的类型。例如,在
1const fn3: {f: (number) => void} = {f: (x) => {x as string}};
1:48-1:48: Cannot cast `x` to string because number [1] is incompatible with string [2]. [incompatible-cast]
Flow 将推断 number
作为 x
的类型,因此强制转换失败。
函数返回类型
与函数参数不同,函数的返回类型通常不需要注释。因此,上面定义的 getLength
不会引发任何 Flow 错误。
但是,此规则有两个值得注意的例外。第一个是类方法。如果我们在 WrappedString
类中包含一个返回内部 data
属性的 getString
方法
1class WrappedString {2 data: string;3 getString(x: string) { 4 return this.data;5 }6}
3:23-3:22: Missing an annotation on return. [missing-local-annot]
Flow 会抱怨 getString
在返回上缺少注释。
第二个例外是递归定义。一个简单的例子是
1function foo() { 2 return bar();3}4
5function bar() {6 return foo();7}
1:1-1:14: The following definitions recursively depend on each other, and Flow cannot compute their types: - function [1] depends on other definition [2] - function [3] depends on other definition [4] Please add type annotations to these definitions [5] [6] [definition-cycle]
上面的代码会引发 [definition-cycle]
错误,该错误指向构成依赖循环的两个位置,即两个缺少的返回注释。在任一函数中添加返回注释将解决此问题。
实际上,对方法返回进行注释的要求是递归定义限制的特例。递归可以通过对 this
的访问来实现。
泛型调用
在对 泛型函数 的调用中,结果的类型可能取决于作为参数传入的值的类型。本节讨论如何计算此结果,以及何时不显式提供类型参数。
例如,考虑以下定义
declare function map<T, U>(
f: (T) => U,
array: $ReadOnlyArray<T>,
): Array<U>;
以及一个使用参数 x => x + 1
和 [1, 2, 3]
的潜在调用
map(x => x + 1, [1, 2, 3]);
这里 Flow 推断 x
的类型为 number
。
泛型调用的其他一些常见示例是调用泛型 Set
类 的构造函数或调用 React 库中的 useState
1const set = new Set([1, 2, 3]);2
3import {useState} from 'react';4const [num, setNum] = useState(42);
4:23-4:34: Cannot call hook [1] because React hooks can only be called within components or hooks. [react-rule-hook]
这里 Flow 推断 set
的类型为 Set<number>
,并且 num
和 setNum
分别为 number
和 (number) => void
。
计算解决方案
计算泛型调用的结果相当于
- 为
T
和U
想出一个不包含泛型部分的解决方案, - 用解决方案替换
map
签名中的T
和U
,以及 - 对
map
的此新签名进行调用。
此过程的设计考虑了两个目标
- 健壮性。当我们到达步骤 (3) 时,结果需要导致正确的调用。
- 完整性。Flow 生成的类型需要尽可能精确和信息丰富,以确保程序的其他部分能够成功检查。
让我们看看这两个目标如何在上面的 map
示例中发挥作用。
Flow 检测到 $ReadOnlyArray<T>
需要与 [1, 2, 3]
的类型兼容。因此,它可以推断出 T
是 number
。
有了 T
的知识,它现在可以成功地检查 x => x + 1
。参数 x
按上下文类型化为 number
,因此结果 x + 1
也是一个数字。此最终约束使我们能够将 U
也计算为 number
。
用上述解决方案替换泛型部分后,map
的新签名为
(f: (number) => number, array: $ReadOnlyArray<number>) => Array<number>
很容易看出调用将被成功检查。
多态调用期间的错误
如果上述过程顺利进行,则您应该不会看到与调用相关的任何错误。但是,当此过程失败时会发生什么?
此过程可能失败有两个原因
类型参数约束不足
在某些情况下,Flow 可能没有足够的信息来决定类型参数的类型。让我们再次检查对内置泛型 Set
类 构造函数的调用,这次不传递任何参数
1const set = new Set(); 2set.add("abc");
1:17-1:19: Cannot call `Set` because `T` [1] is underconstrained by new `Set` [2]. Either add explicit type arguments or cast the expression to your expected type. [underconstrained-implicit-instantiation]
在调用 new Set
期间,我们没有提供足够的信息让 Flow 确定 T
的类型,即使后续对 set.add
的调用清楚地表明 T
将是一个字符串。请记住,类型参数的推断是针对调用的,因此 Flow 不会尝试向前查看后面的语句来确定这一点。
在没有信息的情况下,Flow 可以自由地推断任何类型作为 T
:any
、mixed
、empty
等。这种决定是不可取的,因为它会导致意想不到的结果。例如,如果我们默默地决定使用 Set<empty>
,那么对 set.add("abc")
的调用将因 string
和 empty
之间的兼容性问题而失败,而没有明确指出 empty
来自哪里。
因此,在这种情况下,您将收到 [underconstrained-implicit-instantiation]
错误。解决此错误的方法是添加类型注释。有几种潜在的方法可以做到这一点
以两种方式之一在调用点添加注释
- 显式类型参数
const set = new Set<string>();
- 在初始化变量上添加注释
const set: Set<string> = new Set();
- 显式类型参数
在类定义中对类型参数
T
添加默认类型declare class SetWithDefault<T = string> extends $ReadOnlySet<T> {
constructor(iterable?: ?Iterable<T>): void;
// more methods ...
}在调用点没有类型信息的情况下,Flow 将使用
T
的默认类型作为推断的类型参数const defaultSet = new SetWithDefault(); // defaultSet is SetWithDefault<string>
不兼容错误
即使 Flow 设法为泛型调用中的类型参数推断出非泛型类型,这些类型也可能在当前调用或后面的代码中导致不兼容性。
例如,如果我们对 map
进行以下调用
1declare function map<T, U>(f: (T) => U, array: $ReadOnlyArray<T>): Array<U>;2map(x => x + 1, [{}]);
2:10-2:14: Cannot use operator `+` with operands object literal [1] and number [2] [unsafe-addition]
Flow 将推断 T
为 {}
,因此将 x
类型化为 {}
。这将在检查箭头函数时导致错误,因为不允许在对象上进行 +
操作。
最后,错误的常见来源是泛型调用中推断的类型对于调用本身是正确的,但并不代表代码稍后预期使用的类型。例如,考虑
1import {useState} from 'react';2const [str, setStr] = useState(""); 3
4declare const maybeString: ?string;5setStr(maybeString);
2:23-2:34: Cannot call hook [1] because React hooks can only be called within components or hooks. [react-rule-hook]5:8-5:18: Cannot call `setStr` with `maybeString` bound to the first parameter because: [incompatible-call] Either null or undefined [1] is incompatible with function type [2]. Or null or undefined [1] is incompatible with string [3].
将字符串 ""
传递给对 useState
的调用会导致 Flow 推断出状态的类型为 string
。因此,setStr
在稍后调用时也需要 string
作为输入,因此传递 ?string
将是一个错误。
同样,要修复此错误,只需在调用 useState
时注释预期的“更宽”状态类型即可
const [str, setStr] = useState<?string>("");
空数组字面量
空数组字面量 ([]
) 在 Flow 中有特殊的处理方式。您可以阅读有关其行为和要求的信息。