Dynamic Type Tests

Flow understands many idiomatic constructs used to determine the type of a value at runtime, and incorporates that knowledge into its static analysis.

There are several dynamic type tests (predicates) on local variables that Flow recognizes and uses to refine types. Refining a type with a predicate means narrowing the original type with the type satisfied by values satisfying the predicate.

Type tests can occur in if and switch statements, the test block in loop constructs like for, for-in, for-of, and do-while, conditional expressions (ternary statements), and inline logical expressions like a && a.b.

Maybe, Null, and Undefined#

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function maybe_test(x: ?string): string {
  if (x == null) {
    // The condition will pass if `x` is `null` or `undefined`.
    return x;
  } else {
    // In this branch, `x` must be a string.
    return x;
  }
}

function null_undefined_tests(x: ?string): string {
  if (x === null) {
    // The condition will pass if `x` is `null`.
    return x;
  } else if (x === undefined) {
    // The condition will pass if `x` is `undefined`.
    return x;
  } else {
    // In this branch, `x` must be a string.
    return x;
  }
}
show Flow output hide Flow output
$> flow
4:     return x;
              ^ null. This type is incompatible with the expected return type of
1: function maybe_test(x: ?string): string {
                                    ^^^^^^ string

4:     return x;
              ^ undefined. This type is incompatible with the expected return type of
1: function maybe_test(x: ?string): string {
                                    ^^^^^^ string

14:     return x;
               ^ null. This type is incompatible with the expected return type of
11: function null_undefined_tests(x: ?string): string {
                                               ^^^^^^ string

17:     return x;
               ^ undefined. This type is incompatible with the expected return type of
11: function null_undefined_tests(x: ?string): string {
                                               ^^^^^^ string

Read more about Maybe Types.

Truthiness and Existence#

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
function boolean_truthiness(x: boolean): true {
  if (x) {
    // In this branch, `x` must be `true`.
    return x;
  } else {
    // Flow understands that `x` must be `false` in this branch, and therefore
    // that the expression !x must be `true`.
    return !x;
  }
}

function string_truthiness(x: string): "" {
  if (x) {
    // Flow understands that `x` can be any non-empty string in this branch.
    return x;
  } else {
    // Flow understands that `x` can only be "" in this branch.
    return x;
  }
}

function number_truthiness(x: number): 0 {
  if (x) {
    // Flow understands that `x` can be any non-zero number in this branch.
    return x;
  } else {
    // Flow understands that `x` can only be 0 in this branch.
    return x;
  }
}

function sketchy_null_check(x: ?string): string {
  // Since "" is not truthy, we will replace "" with "default" in this function.
  // Currently Flow does not complain about this pattern, but it's a common
  // request which may be added in the future.
  if (x) {
    return x;
  } else {
    return "default";
  }
}
show Flow output hide Flow output
$> flow
15:     return x;
               ^ string. Expected string literal ``
12: function string_truthiness(x: string): "" {
                                           ^^ string literal ``

25:     return x;
               ^ number. Expected number literal `0`
22: function number_truthiness(x: number): 0 {
                                           ^ number literal `0`

typeof#

This type test is particularly useful in conjunction with union types.

function typeof_test(x: number | string): number {
  if (typeof x === "string") {
    // In this branch, `x` must be a string, and thus has a `length` method.
    return x.length;
  } else {
    // By deduction, `x` must be a number in this branch.
    return x;
  }
}

In JavaScript, typeof null is "object", but don’t worry, Flow won’t let you make that common mistake. (Hint: use x == null instead.)

1
2
3
4
5
6
7
function typeof_null(x: ?Object): Object {
  if (typeof x === "object") {
    return x; // x can still be null
  } else {
    return {};
  }
}
show Flow output hide Flow output
$> flow
3:     return x; // x can still be null
              ^ null. This type is incompatible with the expected return type of
1: function typeof_null(x: ?Object): Object {
                                     ^^^^^^ object type

Array.isArray#

type NestedArray<T> = Array<T|NestedArray<T>>

function flatten<T>(xs: NestedArray<T>): Array<T> {
  let result = [];
  for (let x of xs) {
    if (Array.isArray(x)) {
      // In this branch, `x` must be a `NestedArray<T>`
      result.push(...flatten(x));
    } else {
      // By deduction, `x` must be a `T` in this branch.
      result.push(x);
    }
  }
  return result;
}

A instanceof B#

1
2
3
4
5
6
7
8
9
10
11
12
declare function businessLogic(x: string): void;

function myEventHandler(e: Event) {
  // We only know that e.target is an EventTarget
  e.target.value;
  if (e.target instanceof HTMLInputElement) {
    // Now we know it's an <input />, with a `value` property.
    businessLogic(e.target.value);
  } else {
    // error handling
  }
}
show Flow output hide Flow output
$> flow
5:   e.target.value;
              ^^^^^ property `value`. Property not found in
5:   e.target.value;
     ^^^^^^^^ EventTarget

Tagged Unions#

type BinaryTree =
  { kind: "leaf", value: number } |
  { kind: "branch", left: BinaryTree, right: BinaryTree }

function sumLeaves(tree: BinaryTree): number {
  if (tree.kind === "leaf") {
    return tree.value;
  } else {
    return sumLeaves(tree.left) + sumLeaves(tree.right);
  }
}

Caveats#

Flow is pessimistic about refinements. If it is possible that a refinement may become invalid, Flow will throw away the refinement. This can often happen when invoking a function that might refer to the refined value.

1
2
3
4
5
6
7
8
9
10
declare function something(): void;

function foo(x: { y: ?string }): string {
  if (x.y) {
    something();
    return x.y; // error: x.y may be null/undefined
  } else {
    return "default";
  }
}
show Flow output hide Flow output
$> flow
6:     return x.y; // error: x.y may be null/undefined
              ^^^ null. This type is incompatible with the expected return type of
3: function foo(x: { y: ?string }): string {
                                    ^^^^^^ string

6:     return x.y; // error: x.y may be null/undefined
              ^^^ undefined. This type is incompatible with the expected return type of
3: function foo(x: { y: ?string }): string {
                                    ^^^^^^ string

In the above code, something might mutate x, invalidating the refinement. It is unsafe to expect that x.y will always be a string after calling this function. It is simple to work around this, however. You can copy the object’s property value to a local variable, which can’t be mutated from the outside.

declare function something(): void;

function foo(x: { y: ?string }): string {
  if (x.y) {
    var y = x.y;
    something();
    return y; // OK: something couldn't have changed y
  } else {
    return "default";
  }
}

Another way to help Flow keep a refinement is to use a const binding.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function foo(x: ?string) {
  if (x) {
    () => {
      // We don't know when this function will be invoked, and `null` might be
      // written to `x` before it is.
      (x: string);
    }
  }

  const const_x = x;
  if (const_x) {
    () => {
      // Regardless of when this function is invoked, `null` can never be
      // written to `const_x`, so we can keep the refinement.
      (const_x: string);
    }
  }
}
show Flow output hide Flow output
$> flow
6:       (x: string);
          ^ null. This type is incompatible with
6:       (x: string);
             ^^^^^^ string

6:       (x: string);
          ^ undefined. This type is incompatible with
6:       (x: string);
             ^^^^^^ string

In some cases, Flow will throw away a refinement that is always safe to keep. In the following example, Flow doesn’t have enough information to realize that console.log will not mutate x.

1
2
3
4
5
6
7
8
function foo(x: { y: ?string }): string {
  if (x.y) {
    console.log("*obviously* this doesn't mutate x");
    return x.y; // error: Flow doesn't know that
  } else {
    return "default";
  }
}
show Flow output hide Flow output
$> flow
4:     return x.y; // error: Flow doesn't know that
              ^^^ null. This type is incompatible with the expected return type of
1: function foo(x: { y: ?string }): string {
                                    ^^^^^^ string

4:     return x.y; // error: Flow doesn't know that
              ^^^ undefined. This type is incompatible with the expected return type of
1: function foo(x: { y: ?string }): string {
                                    ^^^^^^ string

Flow does perform a mutation analysis and where it is safe to do so, will preserve refinements after function calls which it knows do not invalidate the refinement.

function bar(x: ?string): string {
  function baz() { /* this doesn't mutate x */ }
  if (x) {
    baz();
    return x; // Flow understands that `baz` can't invalidate the refinement.
  } else {
    return "default";
  }
}

← Prev Next →

You can edit this page on GitHub and send us a pull request!