文件签名(类型优先)
Flow 通过按依赖顺序分别处理每个文件来检查代码库。对于每个包含重要类型信息的检查过程文件,都需要提取签名并存储在主内存中,以便用于依赖它的文件。Flow 依赖于文件边界处的可用注解来构建这些签名。我们称 Flow 架构的这一要求为类型优先。
这种架构的好处是双重的
它极大地提高了性能,特别是在重新检查方面。假设我们希望 Flow 检查一个文件
foo.js
,而它还没有检查其依赖项。Flow 只需查看导出周围的注解即可提取依赖项签名。此过程主要是句法的,因此比旧版本的 Flow(早于 v0.125)用于生成签名的完整类型推断要快得多。它提高了错误可靠性。推断的类型通常会变得很复杂,并可能导致在远离实际源头的下游文件中报告错误。文件边界处的类型注解可以帮助将此类错误本地化,并在引入它们的文件中解决它们。
这种性能优势的权衡是,代码的导出部分需要用类型进行注解,或者需要是类型可以轻松推断的表达式(例如数字和字符串)。
有关类型优先架构的更多信息,请参阅这篇文章。
如何将您的代码库升级到类型优先
注意:类型优先自 v0.134 起成为默认模式,自 v0.143 起成为唯一可用的模式。从那时起,无需任何
.flowconfig
选项来启用它。如果您要从更早的版本升级代码库,以下是一些有用的工具。
升级 Flow 版本
类型优先模式正式发布在 0.125 版本中,但从 0.102 版本开始就以实验性状态提供。如果您当前使用的是旧版本的 Flow,则需要先升级 Flow。使用最新版本的 Flow 是从上述性能优势中获益的最佳方式。
为类型优先准备您的代码库
类型优先需要在模块边界处进行注解,以便为文件构建类型签名。如果缺少这些注解,则会引发 signature-verification-failure
,并且相应代码部分的导出类型将为 any
。
要查看哪些类型丢失以使您的代码库类型优先就绪,请将以下行添加到 .flowconfig
文件的 [options]
部分
well_formed_exports=true
例如,考虑一个导出对 foo
的函数调用的文件 foo.js
declare function foo<T>(x: T): T;
module.exports = foo(1);
函数调用的返回值类型目前无法轻松推断(由于多态性、重载等功能)。它们的返回值需要进行注解,因此您会看到以下错误
Cannot build a typed interface for this module. You should annotate the exports
of this module with types. Cannot determine the type of this call expression. Please
provide an annotation, e.g., by adding a type cast around this expression.
(`signature-verification-failure`)
4│ module.exports = foo(1);
^^^^^^
要解决此问题,您可以添加以下注解
declare function foo<T>(x: T): T;
module.exports = foo(1) as number;
注意:从 0.134 版本开始,类型优先是默认模式。此模式会自动启用
well_formed_exports
,因此您会在没有显式设置此标志的情况下看到这些错误。建议在此升级过程中设置types_first=false
。
密封您的中间结果
随着您在代码库中添加类型的进度,您可以包含目录,以便它们不会随着新代码的提交而发生倒退,直到整个项目都具有格式良好的导出。您可以通过将以下行添加到您的 .flowconfig 来实现此目的
well_formed_exports.includes=<PROJECT_ROOT>/path/to/directory
警告:这是一个子字符串检查,而不是正则表达式(出于性能原因)。
用于大型代码库的代码修改
向大型代码库添加必要的注解可能非常繁琐。为了减轻这种负担,我们提供了一个基于 Flow 推断的代码修改,它可以用于批量注解多个文件。有关更多信息,请参阅本教程。
启用类型优先标志
消除签名验证错误后,您可以通过将以下行添加到 .flowconfig
文件的 [options]
部分来启用类型优先模式
types_first=true
您也可以将 --types-first
传递给 flow check
或 flow start
命令。
之前的 well_formed_exports
标志由 types_first
隐式包含。完成此过程并启用类型优先后,您可以删除 well_formed_exports
。
不幸的是,无法为存储库的一部分启用类型优先模式;此切换会影响当前 .flowconfig
管理的所有文件。
注意:上述标志在 Flow
>=0.102
版本中可用,带有experimental.
前缀(在 v0.128 之前,它使用whitelist
代替includes
)experimental.well_formed_exports=true
experimental.well_formed_exports.whitelist=<PROJECT_ROOT>/path/to/directory
experimental.types_first=true
注意:如果您使用的是默认启用类型优先的 Flow 版本(即
>=0.134
),请确保在运行代码修改时在您的 .flowconfig 中设置types_first=false
。
处理新引入的错误
在经典模式和类型优先模式之间切换可能会导致一些新的 Flow 错误,除了我们之前提到的签名验证失败之外。这些错误是由于基于注解的类型的解释方式与它们各自的推断类型不同而造成的。
以下是一些常见的错误模式以及如何克服它们。
在导出中将数组元组视为普通数组
在类型优先中,导出位置的数组字面量
module.exports = [e1, e2];
被视为具有类型 Array<t1 | t2>
,其中 e1
和 e2
的类型分别为 t1
和 t2
,而不是元组类型 [t1, t2]
。
在经典模式中,推断的类型同时包含两种类型。这可能会导致在导入期望找到例如在导入的第一个位置找到类型 t1
的文件的错误。
修复:如果期望元组类型,则需要在导出端显式添加注解 [t1, t2]
。
导出中的间接对象赋值
Flow 允许以下代码
function foo(): void {}
foo.x = () => {};
foo.x.y = 2;
module.exports = foo;
但在类型优先中,导出的类型将为
{
(): void;
x: () => void;
}
换句话说,它不会考虑对 y
的更新。
修复:要将对 y
的更新包含在导出的类型中,需要用类型对导出进行注解
{
(): void;
x: { (): void; y: number; };
};
对于更复杂的赋值模式,例如
function foo(): void {}
Object.assign(foo, { x: 1});
module.exports = foo;
您需要手动用 { (): void; x: number }
对导出进行注解,或者对函数定义之前的赋值进行注解
foo.x = 1;
function foo(): void {}
module.exports = foo;
请注意,在最后一个示例中,如果 Flow 类型优先是在定义之后进行静态更新,则它会获取静态更新
function foo(): void {}
foo.x = 1;
module.exports = foo;
带有更新的导出变量
类型优先签名提取器不会获取导出 let 绑定变量的后续更新。考虑以下示例
let foo: number | string = 1;
foo = "blah";
module.exports = foo;
在经典模式中,导出的类型将为 string
。在类型优先中,它将为 number | string
,因此如果下游类型依赖于更精确的类型,则您可能会遇到一些错误。
修复:在更新上引入一个新变量,并导出该变量。例如
const foo1: number | string = 1;
const foo2 = "blah";
module.exports = foo2;