Type Refinements
Refinements allow us to narrow the type of a value based on conditional tests.
For example, in the function below value
is a union of "A"
or "B"
.
1function func(value: "A" | "B") {2 if (value === "A") {3 value as "A";4 }5}
Inside of the if
block we know that value must be "A"
because that's the only
time the if-statement will be true.
The ability for a static type checker to be able to tell that the value inside
the if statement must be "A"
is known as a refinement.
Next we'll add an else
block to our if statement.
1function func(value: "A" | "B") {2 if (value === "A") {3 value as "A";4 } else {5 value as "B";6 }7}
Inside of the else
block we know that value must be "B"
because it can only
be "A"
or "B"
and we've removed "A"
from the possibilities.
Ways to refine in Flow
typeof
checks
You can use a typeof value === "<type>"
check to refine a value to one of the categories supported by the typeof
operator.
The typeof
operator can output "undefined"
,"boolean"
, "number"
, "bigint"
, "string"
, "symbol"
, "function"
, or "object"
.
Keep in mind that the typeof
operator will return "object"
for objects, but also null
and arrays as well.
1function func(value: mixed) {2 if (typeof value === "string") {3 value as string;4 } else if (typeof value === "boolean") {5 value as boolean;6 } else if (typeof value === "object") {7 // `value` could be null, an array, or an object8 value as null | interface {} | $ReadOnlyArray<mixed>;9 }10}
To check for null
, use a value === null
equality check.
1function func(value: mixed) {2 if (value === null) {3 value as null; // `value` is null4 }5}
To check for arrays, use Array.isArray
:
1function func(value: mixed) {2 if (Array.isArray(value)) {3 value as $ReadOnlyArray<mixed>; // `value` is an array4 }5}
Equality checks
As shown in the introductory example, you can use an equality check to narrow a value to a specific type.
This also applies to equality checks made in switch
statements.
1function func(value: "A" | "B" | "C") {2 if (value === "A") {3 value as "A";4 } else {5 value as "B" | "C";6 }7
8 switch (value) {9 case "A":10 value as "A";11 break;12 case "B":13 value as "B";14 break;15 case "C":16 value as "C";17 break;18 }19}
While in general it is not recommended to use ==
in JavaScript, due to the coercions it performs,
doing value == null
(or value != null
) checks value
exactly for null
and void
.
This works well with Flow's maybe types, which create a union with null
and void
.
1function func(value: ?string) {2 if (value != null) {3 value as string;4 } else {5 value as null | void;6 }7}
You can refine a union of object types based on a common tag, which we call disjoint object unions:
1type A = {type: "A", s: string};2type B = {type: "B", n: number};3
4function func(value: A | B) {5 if (value.type === "A") {6 // `value` is A7 value.s as string; // Works8 } else {9 // `value` is B10 value.n as number; // Works11 }12}
Truthiness checks
You can use non-booleans in JavaScript conditionals.
0
, NaN
, ""
, null
, and undefined
will all coerce to false
(and so are considered "falsey").
Other values will coerce to true
(and so are considered "truthy").
1function func(value: ?string) {2 if (value) {3 value as string; // Works4 } else {5 value as null | void; // Error! Could still be the empty string "" 6 }7}
5:5-5:9: Cannot cast `value` to union type because string [1] is incompatible with union type [2]. [incompatible-cast]
You can see in the above example why doing a truthy check when your value can be a string or number is not suggested:
it is possible to unintentionally check against the ""
or 0
.
We created a Flow lint called sketchy-null to guard against this scenario:
1// flowlint sketchy-null:error2function func(value: ?string) {3 if (value) { // Error! 4 }5}
3:7-3:11: Sketchy null check on string [1] which is potentially an empty string. Perhaps you meant to check for null or undefined [2]? [sketchy-null-string]
instanceof
checks
You can use the instanceof operator to narrow a value as well. It checks if the supplied constructor's prototype is anywhere in a value's prototype chain.
1class A {2 amaze(): void {}3}4class B extends A {5 build(): void {}6}7
8function func(value: mixed) {9 if (value instanceof B) {10 value.amaze(); // Works11 value.build(); // Works12 }13
14 if (value instanceof A) {15 value.amaze(); // Works16 value.build(); // Error 17 }18
19 if (value instanceof Object) {20 value.toString(); // Works21 }22}
16:11-16:15: Cannot call `value.build` because property `build` is missing in `A` [1]. [prop-missing]
Assignments
Flow follows your control flow and narrows the type of a variable after you have assigned to it.
1declare const b: boolean;2
3let x: ?string = b ? "str" : null;4
5x as ?string;6
7x = "hi";8
9// We know `x` must now be a string after the assignment10x as string; // Works
Type Guards
You can create a reusable refinement by defining a function which is a type guard.
1function nonMaybe<T>(x: ?T): x is T { 2 return x != null;3}4
5function func(value: ?string) {6 if (nonMaybe(value)) {7 value as string; // Works!8 }9}
1:30-1:35: Inconsistent type guard declaration. The negation of the predicate encoded in return expression `x != null` [1] needs to completely refine away the guard type `T` [2]. Consider using a one-sided type-guard (`implies x is T`). See 2. in https://flow.org/en/docs/types/type-guards/#toc-consistency-checks-of-type-guard-functions. [incompatible-type-guard]
Refinement Invalidations
It is also possible to invalidate refinements, for example:
1function otherFunc() { /* ... */ }2
3function func(value: {prop?: string}) {4 if (value.prop) {5 otherFunc();6 value.prop.charAt(0); // Error! 7 }8}
6:16-6:21: Cannot call `value.prop.charAt` because property `charAt` is missing in undefined [1]. [incompatible-use]
The reason for this is that we don't know that otherFunc()
hasn't done
something to our value. Imagine the following scenario:
1const obj: {prop?: string} = {prop: "test"};2
3function otherFunc() {4 if (Math.random() > 0.5) {5 delete obj.prop;6 }7}8
9function func(value: {prop?: string}) {10 if (value.prop) {11 otherFunc();12 value.prop.charAt(0); // Error! 13 }14}15
16func(obj);
12:16-12:21: Cannot call `value.prop.charAt` because property `charAt` is missing in undefined [1]. [incompatible-use]
Inside of otherFunc()
we sometimes remove prop
. Flow doesn't know if the
if (value.prop)
check is still true, so it invalidates the refinement.
There's a straightforward way to get around this. Store the value before calling another function and use the stored value instead. This way you can prevent the refinement from invalidating.
1function otherFunc() { /* ... */ }2
3function func(value: {prop?: string}) {4 if (value.prop) {5 const prop = value.prop;6 otherFunc();7 prop.charAt(0);8 }9}