Announcing User Defined Type Guards in Flow

Panagiotis Vekris
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:

  1. The type of the refined parameter after the predicate of the return expression has been applied is a subtype of the guard type.
  2. 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.

--

--