Announcing User Defined Type Guards in Flow
Flow now lets you define a function that encodes a type predicate over its parameter. This predicate, which we refer to as a type guard, is annotated in place of a return type annotation as x is PredicateType
. It declares that if the function returns true
then its parameter x
is of type PredicateType
.
The syntax for such a function, similar to how it appears in TypeScript, is
function predicate(x: InputType): x is PredicateType {
return <some_expression>;
}
The library definition for Array .filter
has also been updated to recognize callbacks with type (x: Input) => x is PredType
and refine arrays of type Array<Input>
to Array<PredType>
.
Background
The ability to use Array .filter
to produce arrays with refined types has been one of the most commonly requested features in feedback that the team has gathered over the past several years.
Predicate functions based on the %checks
annotation are not a good fit for higher order functions, such as Array’s .filter
method, and so they do not support this use case. A feature that relies on typing, such as type guards, is a more natural way of expressing this refinement.
At the same time, the refinement based on %checks
functions has been known to be hard-to-understand and spotty. Robust support for refining using function predicates is essential for a dynamic language like JavaScript.
Basic Usage
Let's see a simple example where we define a type guard function over a data type
type A = { type: "A"; data: string };
type B = { type: "B"; data: number };
type AorB = A | B;
function isA(value: AorB): value is A {
return value.type === "A";
}
Function isA
will return true if (and only if) its input value
has type A
. It can therefore be used to refine values of type AorB
down to A
:
function test(x: AorB) {
if (isA(x)) {
// `x` has now been refined to type A.
// We can assign to it variables of type A ...
const y: A = x;
// ...and access A's properties through `x`
const stringData: string = x.data;
// As a sanity check, the following assignment to B will error
const error: B = x;
}
}
Array .filter
Flow now recognizes when you call filter
on an array of type Array<T>
, with a callback function with type (value: T) => value is S
. It returns an array of type Array<S>
. Note that S
needs to be a subtype of the type of the array element T
.
Here are some examples
const isNonMaybe = <A>(x: ?A): x is A => x != null;
function filterNull(response: Array<?number>): Array<number> {
return response.filter(isNonMaybe); // no error
}
// Using the definitions from above
function filterAs(response: Array<AorB>): Array<A> {
return response.filter(isA); // no error
}
With proper filtering support, it is now possible to move away from the error-prone pattern of using arr.filter(Boolean)
to filter out non-null
values from array arr
. Instead, a more correct arr.filter(isNonMaybe)
can be used. The difference here is that Boolean
will also remove from the array all falsy values (including for example 0
, ""
, false
), instead of just null
and undefined
.
Defining Type Guard Functions
Flow runs a number of checks to ensure that type guard functions respect their declared predicate. Most of them are unsurprising and you can find details about them in the docs.
The most interesting check is that of consistency of the declared predicate type with the check happening in the body of the function. Specifically Flow establishes two things:
- The type of the refined parameter after the predicate of the return expression has been applied is a subtype of the guard type.
- The refined parameter is not reassigned in the function body.
The following examples will be errors:
function numOrStrError(x: mixed): x is number | string {
return (typeof x === "number"
|| typeof x === "boolean"); // error boolean ~> string
}
function isNumberError1(x: mixed): x is number {
x = 1;
return typeof x === "number"; // error x is written to
}
function isNumberError2(x: mixed): x is number { // error x is reassigned
function foo() {
x = 1;
}
foo();
return typeof x === "number";
}
Note that TypeScript does not perform such check.
Migration From %checks
Developers are encouraged to convert existing instances of %checks
to the more robust and feature-rich type guard syntax when possible.
This should be straightforward in most cases. For example,
const isA = (x: mixed): boolean %checks => x instanceof A;
const isNonMaybe = (x: mixed): boolean %checks => x != null;
can respectively be written as
const isA = (x: mixed): x is A => x instanceof A;
const isNonMaybe = <A>(x: ?A): x is A => x != null;
A couple cases where a larger refactoring needs to happen:
%checks
-functions that refine multiple input parameters, since type-guard syntax can only refine a single parameter. (try-Flow)- Negations of complex predicates, since type-guards need to refer to the entire condition at once. See this code for example.
Adoption
See this section of our docs for what versions of tooling support the new syntax.