Annotation Requirement
Note: As of version 0.199 Flow uses Local Type Inference as its inference algorithm. The rules in this section reflect the main design features in this inference scheme.
Flow tries to avoid requiring type annotation for parts of programs where types can easily be inferred from the immediate context of an expression, variable, parameter, etc.
Variable declarations
Take for example the following variable definition
const len = "abc".length;
All information necessary to infer the type of len
is included in the initializer
"abc".length
. Flow will first determine that "abc"
is a string, and then that the
length
property of a string is a number.
The same logic can be applied for all const
-like initializations. Where things
get a little more complicated is when variable initialization spans across multiple statements,
for example in
1declare const maybeString: ?string;2
3let len;4if (typeof maybeString === "string") {5 len = maybeString.length;6} else {7 len = 0;8}
Flow can still determine that len
is a number
, but in order to do so it looks
ahead to multiple initializer statements. See section on variable declarations
for details on how various initializer patterns determine the type of a variable,
and when an annotation on a variable declaration is necessary.
Function Parameters
Unlike variable declarations, this kind of "lookahead" reasoning cannot be used to determine the type of function parameters. Consider the function
function getLength(x) {
return x.length;
}
There are many kinds of x
on which we could access and return a length
property:
an object with a length
property, or a string, just to name a few. If later on in
the program we had the following calls to getLength
getLength("abc");
getLength({length: 1});
one possible inference would be that x
is a string | { length: number }
. What this implies,
however, is that the type of getLength
is determined by any part of the current
program. This kind of global reasoning can lead to surprising action-at-a-distance
behavior, and so is avoided. Instead, Flow requires that function parameters are annotated. Failure to
provide such a type annotation manifests as a [missing-local-annot]
error on the parameter x
,
and the body of the function is checked with 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]
To fix this error, one can simply annotate x
as
1function getLength(x: string) {2 return x.length;3}
The same requirement holds for class methods
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]
Contextual Typing
Function parameters do not always need to be explicitly annotated. In the case of a callback function to a function call, the parameter type can easily be inferred from the immediate context. Consider for example the following code
const arr = [0, 1, 2];
const arrPlusOne = arr.find(x => x % 2 === 1);
Flow infers that the type of arr
is Array<number>
. Combining this with the builtin
information for Array.find
, Flow can determine that the type of x => x % 2 + 1
needs to be number => mixed
. This type acts as a hint for Flow and provides enough
information to determine the type of x
as number
.
Any attendant annotation can potentially act as a hint to a function parameter, for example
1const fn1: (x: number) => number = x => x + 1;
However, it is also possible that an annotation cannot be used as a function parameter hint:
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]
In this example the mixed
type simply does not include enough information to
extract a candidate type for x
.
Flow can infer the types for unannotated parameters even when they are nested within other expressions like objects. For example in in
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 will infer number
as the type of x
, and so the cast fails.
Function Return Types
Unlike function parameters, a function's return type does not need to be annotated in general.
So the above definition of getLength
won't raise any Flow errors.
There are, however, a couple of notable exceptions to this rule. The first one is
class methods. If we included to the WrappedString
class a getString
method
that returns the internal data
property:
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 would complain that getString
is missing an annotation on the return.
The second exception is recursive definitions. A trivial example of this would be
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]
The above code raises a [definition-cycle]
error, which points to the two locations
that form a dependency cycle, the two missing return annotations. Adding
a return annotation to either function would resolve the issue.
Effectively, the requirement on an annotation for method returns is a special-case
of the recursive definition restriction. The recursion is possible through access on
this
.
Generic Calls
In calls to generic functions the type of the result may depend on the types of the values passed in as arguments. This section discusses how this result is computed, when type arguments are not explicitly provided.
Consider for example the definition
declare function map<T, U>(
f: (T) => U,
array: $ReadOnlyArray<T>,
): Array<U>;
and a potential call with arguments x => x + 1
and [1, 2, 3]
:
map(x => x + 1, [1, 2, 3]);
Here Flow infers that the type of x
is number
.
Some other common examples of generic calls are calling the constructor of the generic
Set
class
or calling useState
from the React library:
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. (https://react.dev/reference/rules/rules-of-hooks) [react-rule-hook]
Flow here infers that the type of set
is Set<number>
, and that num
and setNum
are number
and (number) => void
, respectively.
Computing a Solution
Computing the result of a generic call amounts to:
- coming up with a solution for
T
andU
that does not contain generic parts, - replacing
T
andU
with the solution in the signature ofmap
, and - performing a call to this new signature of
map
.
This process is designed with two goals in mind:
- Soundness. The results need to lead to a correct call when we reach step (3).
- Completeness. The types Flow produces need to be as precise and informative as possible, to ensure that other parts of the program will be successfully checked.
Let's see how these two goals come into play in the map
example from above.
Flow detects that $ReadOnlyArray<T>
needs to be compatible with the type of [1, 2, 3]
.
It can therefore infer that T
is number
.
With the knowledge of T
it can now successfully check x => x + 1
. The parameter x
is contextually typed as number
, and thus the result x + 1
is also a number.
This final constraint allows us to compute U
as a number
too.
The new signature of map
after replacing the generic parts with the above solution
is
(f: (number) => number, array: $ReadOnlyArray<number>) => Array<number>
It is easy to see that the call would be successfully checked.
Errors during Polymorphic Calls
If the above process goes on smoothly, you should not be seeing any errors associated with the call. What happens though when this process fails?
There are two reasons why this process could fail:
Under-constrained Type Parameters
There are cases where Flow might not have enough information to decide the type of a type parameter.
Let's examine again a call to the builtin generic
Set
class
constructor, this time without passing any arguments:
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]
During the call to new Set
, we are not providing enough information for Flow to
determine the type for T
, even though the subsequent call to set.add
clearly
implies that T
will be a string. Remember that inference of type arguments is
local to the call, so Flow will not attempt to look ahead in later statements
to determine this.
In the absence of information, Flow would be at liberty to infer any type
as T
: any
, mixed
, empty
, etc.
This kind of decision is undesirable, as it can lead to surprising results.
For example, if we silently decided on Set<empty>
then the call to set.add("abc")
would
fail with an incompatibility between string
and empty
, without a clear indication
of where the empty
came from.
So instead, in situations like this, you'll get an [underconstrained-implicit-instantiation]
error.
The way to fix this error is by adding a type annotation. There a few potential ways to do this:
Add an annotation at the call-site in one of two ways:
- an explicit type argument
const set = new Set<string>();
- an annotation on the initialization variable:
const set: Set<string> = new Set();
- an explicit type argument
Add a default type on the type parameter
T
at the definition of the class:declare class SetWithDefault<T = string> extends $ReadOnlySet<T> {
constructor(iterable?: ?Iterable<T>): void;
// more methods ...
}In the absence of any type information at the call-site, Flow will use the default type of
T
as the inferred type argument:const defaultSet = new SetWithDefault(); // defaultSet is SetWithDefault<string>
Incompatibility Errors
Even when Flow manages to infer non-generic types for the type parameters in a generic call, these types might still lead to incompatibilities either in the current call or in code later on.
For example, if we had the following call to 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 will infer T
as {}
, and therefore type x
as {}
. This will cause an error when checking the arrow function
since the +
operation is not allowed on objects.
Finally, a common source of errors is the case where the inferred type in a generic call is correct for the call itself, but not indicative of the expected use later in the code. For example, consider
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. (https://react.dev/reference/rules/rules-of-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].
Passing the string ""
to the call to useState
makes Flow infer string
as the type
of the state. So setStr
will also expect a string
as input when called later on,
and therefore passing a ?string
will be an error.
Again, to fix this error it suffices to annotate the expected "wider" type of state
when calling useState
:
const [str, setStr] = useState<?string>("");
Empty Array Literals
Empty array literals ([]
) are handled specially in Flow. You can read about their behavior and requirements.