File Signatures (Types-First)
Flow checks codebases by processing each file separately in dependency order. For every file containing important typing information for the checking process, a signature needs to be extracted and stored in main memory, to be used for files that depend on it. Flow relies on annotations available at the boundaries of files to build these signatures. We call this requirement of Flow's architecture Types-First.
The benefit of this 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. 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 that legacy versions of Flow (prior to v0.125) used to perform in order to generate signatures.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 trade-off for this performance benefit is that exported parts of the code need to be annotated with types, or to be expressions whose type can be trivially inferred (for example numbers and strings).
More information on the Types-First architecture can be found in this post.
How to upgrade your codebase to Types-First
Note: Types-first has been the default mode since v0.134 and the only available mode since v0.143. No
.flowconfig
options are necessary to enable it since then. In case you're upgrading your codebase from a much earlier version here are some useful tools.
Upgrade Flow version
Types-first mode was 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) as 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;