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
.
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; } }
$> 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.
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"; } }
$> 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.)
function typeof_null(x: ?Object): Object { if (typeof x === "object") { return x; // x can still be null } else { return {}; } }
$> 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
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; }
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 } }
$> flow
5: e.target.value; ^^^^^ property `value`. Property not found in 5: e.target.value; ^^^^^^^^ EventTarget
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); } }
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.
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"; } }
$> 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.
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); } } }
$> 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
.
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"; } }
$> 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"; } }
© 2013–present Facebook Inc.
Licensed under the BSD License.
https://flowtype.org/docs/dynamic-type-tests.html