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.
Both Flow and TypeScript narrow via typeof / instanceof / equality / type guards, but the rules for when a refinement is dropped diverge. See Flow and TypeScript refinement invalidation comparison for more.
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: unknown) {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<unknown>;9 }10}To check for null, use a value === null equality check.
1function func(value: unknown) {2 if (value === null) {3 value as null; // `value` is null4 }5}To check for arrays, use Array.isArray:
1function func(value: unknown) {2 if (Array.isArray(value)) {3 value as ReadonlyArray<unknown>; // `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 ""incompatible-typeCannot cast value to union type because string [1] is incompatible with union type [2].6 }7}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!sketchy-null-stringSketchy null check on string [1] which is potentially an empty string. Perhaps you meant to check for null or undefined [2]?4 }5}in checks
You can use the in operator to check if a property exists on an object (either in its own properties, or up the prototype chain). This can be used to refine a union of objects:
1function func(obj: {foo: string, value: boolean} | {bar: string, value: number}) {2 if ('foo' in obj) {3 obj.value as boolean; // Works!4 } else {5 obj.value as number; // Works!6 }7}This works best on unions of exact objects, since in the negation we know the property does not exist. We cannot say the same for inexact objects, interfaces, and instance types, since they may have other unknown properties, including the one we are checking. Additionally, optional properties may or may not exist, so are not particularly useful to check against.
If you want to refine a union of tuple types based on whether an element exists,
check the length property instead of attempting to use in.
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: unknown) {9 if (value instanceof B) {10 value.amaze(); // Works11 value.build(); // Works12 }13
14 if (value instanceof A) {15 value.amaze(); // Works16 value.build(); // Errorprop-missingCannot call value.build because property build is missing in A [1].17 }18
19 if (value instanceof Object) {20 value.toString(); // Works21 }22}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; // WorksType Guards
You can create a reusable refinement by defining a function which is a type guard.
1function nonMaybe<T>(x: ?T): implies x is T {2 return x != null;3}4
5function func(value: ?string) {6 if (nonMaybe(value)) {7 value as string; // Works!8 }9}Limitations
Refinement Invalidations
Flow drops a refinement when intervening code could have changed the underlying value at the refined storage location. Concretely, a refinement is invalidated when:
- The refined binding or property is written to (
x = ...,obj.k = ...). - A refinement on an object property is in scope, and the property is reachable through aliasing or could be mutated by a callee (a bare call between the check and the use drops the refinement).
- A refinement on a binding captured by a closure is in scope, and an intervening call could invoke that closure.
A bare call to a function that does not visibly touch the refined location does not, on its own, drop a refinement on a local binding. This is the most common over-correction: users assume any function call invalidates refinements when actually only property and closure-captured refinements are conservative across calls.
1declare function sideEffect(): void;2
3function localCase(x: ?number) {4 if (x != null) {5 sideEffect(); // bare call does NOT drop the refinement on a local6 const a: number = x; // OK7 }8}The reason a property refinement is dropped across a call is that we don't know that the callee hasn't mutated the property through an alias. 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!incompatible-useCannot call value.prop.charAt because property charAt is missing in undefined [1].13 }14}15
16func(obj);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.
Marking the parameter type readonly (Readonly<{prop?: string}> or {readonly prop?: string}) doesn't help — readonly only constrains what this code can do through this view, and the callee may hold a non-readonly reference to the same object.
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}Destructured bindings can't be used to refine
When you destructure a value out of a discriminated union, the resulting bindings each carry the full union type independent of one another. Refining the destructured sentinel binding does not narrow its sibling bindings:
1type FormField =2 | {kind: 'text', value: string}3 | {kind: 'number', value: number};4
5declare const field: FormField;6const {kind, value} = field;7if (kind === 'text') {8 const s: string = value; // Error: `value` keeps its full `string | number` typeincompatible-typeCannot assign value to s because number [1] is incompatible with string [2].9}The fix is to refine through the original value instead of destructuring up front:
1type FormField =2 | {kind: 'text', value: string}3 | {kind: 'number', value: number};4
5declare const field: FormField;6if (field.kind === 'text') {7 const s: string = field.value; // OK8}Alternatively, a match expression or statement handles matching and destructuring in one form — each arm matches the sentinel and binds value at its per-arm type:
1type FormField =2 | {kind: 'text', value: string}3 | {kind: 'number', value: number};4
5declare const field: FormField;6match (field) {7 {kind: 'text', const value} => {8 const s: string = value;9 }10 {kind: 'number', const value} => {11 const n: number = value;12 }13}Refining a generic value does not narrow other values sharing the same type parameter
When a function is generic, narrowing one parameter based on a runtime check does not narrow other parameters that share the same type parameter. Flow treats the type parameter as a single, fixed type for the entire call — refining the value of one parameter does not change the type T itself.
1type Row = {kind: 'row', cols: number};2type Grid = {kind: 'grid', rows: number, cols: number};3type Layout = Row | Grid;4
5function updateLayout<T extends Layout>(6 layout: T,7 setLayout: (value: T) => void,8): void {9 if (layout.kind === 'row') {10 // `layout` is narrowed to `Row`, but `setLayout` is still `(T) => void`11 setLayout({kind: 'row', cols: 3}); // ERRORincompatible-typeCannot call setLayout with object literal bound to value because object literal [1] is incompatible with T [2].12 }13}To work around this, avoid the generic and use the concrete union type instead. Refinement then works as expected because setLayout accepts any Layout value:
1type Row = {kind: 'row', cols: number};2type Grid = {kind: 'grid', rows: number, cols: number};3type Layout = Row | Grid;4
5function updateLayout(6 layout: Layout,7 setLayout: (value: Layout) => void,8): void {9 if (layout.kind === 'row') {10 setLayout({kind: 'row', cols: 3});11 }12}See Also
- Type Guards — custom functions that act as user-defined refinements
- Unions — refinements are most commonly used to narrow union types
- Maybe Types — refining nullable values with
!= nullchecks - Refining
unknown— idioms specific to narrowingunknownvalues - Variables — how variable declarations interact with refinement
- Match Expressions — pattern matching with its own narrowing and exhaustiveness checking