Types-First
Flow checks codebases by processing each file separately in dependency order. After a file has been checked, a signature is extracted and stored in main memory, to be used for files that depend on it. Currently, the default mode (we’ll also refer to it as classic mode) builds these signatures by using the types inferred for the file’s exports. In the new types-first architecture, Flow relies on annotations available at the boundaries of files to build these signatures.
The benefit of this new architecture is dual:
-
It dramatically improves performance, in particular when it comes to rechecks. Suppose we want Flow to check a file
foo.js
, for which it hasn’t checked its dependencies yet. Classic mode would need to check all dependencies and generate signatures from them first, before it could checkfoo.js
. In types-first, Flow extracts the dependency signatures just by looking at the annotations around the exports. This process is mostly syntactic, and therefore much faster than full type inference. -
It improves error reliability. Inferred types often become complicated, and may lead to errors being reported in downstream files, far away from their actual source. Type annotations at file boundaries of files can help localize such errors, and address them in the file that introduced them.
The caveat of this new version is that it requires exported parts of the code to be annotated with types, or to be expressions whose type can be trivially inferred (for example numbers and strings).
How to upgrade your codebase to Types-First
Upgrade Flow version
Types-first mode is officially released with version 0.125, but has been available in experimental status as of version 0.102. If you are currently on an older Flow version, you’d have to first upgrade Flow. Using the latest Flow version is the best way to benefit from the performance benefits outlined above.
Prepare your codebase for Types-First
Types-first requires annotations at module boundaries in order to build type signature for files. If these annotations are missing, then a signature-verification-failure
is raised, and the exported type for the respective part of the code will be any
.
To see what types are missing to make your codebase types-first ready, add the following line to the [options]
section of the .flowconfig
file:
well_formed_exports=true
Consider for example a file foo.js
that exports a function call to foo
declare function foo<T>(x: T): T; module.exports = foo(1);
The return type of function calls is currently not trivially inferable (due to features like polymorphism, overloading etc.). Their result needs to be annotated and so you’d see the following error:
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); ^^^^^^
To resolve this, you can add an annotation like the following:
declare function foo<T>(x: T): T; module.exports = (foo(1): number);
Note: As of version 0.134, types-first is the default mode. This mode automatically enables
well_formed_exports
, so you would see these errors without explicitly setting this flag. It is advisable to settypes_first=false
during this part of the upgrade.
Seal your intermediate results
As you make progress adding types to your codebase, you can include directories so that they don’t regress as new code gets committed, and until the entire project has well-formed exports. You can do this by adding lines like the following to your .flowconfig:
well_formed_exports.includes=<PROJECT_ROOT>/path/to/directory
Warning: That this is a substring check, not a regular expression (for performance reasons).
A codemod for large codebases
Adding the necessary annotations to large codebases can be quite tedious. To ease this burden, we are providing a codemod based on Flow’s inference, that can be used to annotate multiple files in bulk. See this tutorial for more.
Enable the types-first flag
Once you have eliminated signature verification errors, you can turn on the types-first mode, by adding the following line to the [options]
section of the .flowconfig
file:
types_first=true
You can also pass --types-first
to the flow check
or flow start
commands.
The well_formed_exports
flag from before is implied by types_first
. Once this process is completed and types-first has been enabled, you can remove well_formed_exports
.
Unfortunately, it is not possible to enable types-first mode for part of your repo; this switch affects all files managed by the current .flowconfig
.
Note: The above flags are available in versions of Flow
>=0.102
with theexperimental.
prefix (and prior to v0.128, it usedwhitelist
in place ofincludes
):experimental.well_formed_exports=true experimental.well_formed_exports.whitelist=<PROJECT_ROOT>/path/to/directory experimental.types_first=true
Note: If you are using a version where types-first is enabled by default (ie.
>=0.134
), make sure you settypes_first=false
in your .flowconfig while running the codemods.
Deal with newly introduced errors
Switching between classic and types-first mode may cause some new Flow errors, besides signature-verification failures that we mentioned earlier. These errors are due differences in the way types based on annotations are interpreted, compared to their respective inferred types.
Below are some common error patterns and how to overcome them.
Array tuples treated as regular arrays in exports
In types-first, an array literal in an export position
module.exports = [e1, e2];
is treated as having type Array<t1 | t2>
, where e1
and e2
have types t1
and t2
, instead of the tuple type [t1, t2]
.
In classic mode, the inferred type encompassed both types at the same time. This might cause errors in importing files that expect for example to find type t1
in the first position of the import.
Fix: If a tuple type is expected, then the annotation [t1, t2]
needs to be explicitly added on the export side.
Indirect object assignments in exports
Flow allows the code
function foo(): void {} foo.x = () => {}; foo.x.y = 2; module.exports = foo;
but in types-first the exported type will be
{ (): void; x: () => void; }
In other words it won’t take into account the update on y
.
Fix: To include the update on y
in the exported type, the export will need to be annotated with the type
{ (): void; x: { (): void; y: number; }; };
The same holds for more complex assignment patterns like
function foo(): void {} Object.assign(foo, { x: 1}); module.exports = foo;
where you’ll need to manually annotate the export with { (): void; x: number }
, or assignments preceding the function definition
foo.x = 1; function foo(): void {} module.exports = foo;
Note that in the last example, Flow types-first will pick up the static update if it was after the definition:
function foo(): void {} foo.x = 1; module.exports = foo;
Exported variables with updates
The types-first signature extractor will not pick up subsequent update of an exported let-bound variables. Consider the example
let foo: number | string = 1; foo = "blah"; module.exports = foo;
In classic mode the exported type would be string
. In types-first it will be number | string
, so if downstream typing depends on the more precise type, then you might get some errors.
Fix: Introduce a new variable on the update and export that one. For example
const foo1: number | string = 1; const foo2 = "blah"; module.exports = foo2;
© 2013–present Facebook Inc.
Licensed under the MIT License.
https://flow.org/en/docs/lang/types-first