Using enums
Flow Enums are not a syntax for union types. They are their own type, and each member of a Flow Enum has the same type. Large union types can cause performance issues, as Flow has to consider each member as a separate type. With Flow Enums, no matter how large your enum is, Flow will always exhibit good performance as it only has one type to keep track of.
We use the following enum in the examples below:
enum Status {
Active,
Paused,
Off,
}
Accessing enum members
Access members with the dot syntax:
const status = Status.Active;
You can’t use computed access:
1enum Status {2 Active,3 Paused,4 Off,5}6const x = "Active";7Status[x]; // Error: computed access on enums is not allowed
7:8-7:8: Cannot access `x` on enum `Status` [1] because computed access is not allowed on enums. [invalid-enum-access]
Using as a type annotation
The enum declaration defines both a value (from which you can access the enum members and methods) and a type of the same name, which is the type of the enum members.
function calculateStatus(): Status {
...
}
const status: Status = calculateStatus();
Casting to representation type
Enums do not implicitly coerce to their representation type or vice-versa.
If you want to convert from the enum type to the representation type, you can use an explicit cast x as string
:
1enum Status {Active, Paused, Off}2
3const s: string = Status.Active; // Error: 'Status' is not compatible with 'string' 4
5declare const status: Status;6const statusString: string = status as string;
3:19-3:31: Cannot assign `Status.Active` to `s` because `Status` [1] is incompatible with string [2]. You can explicitly cast your enum value to a string using `<expr> as string`. [incompatible-type]
You can also call .valueOf()
on the enum value ≥0.234.
This is particularly useful if you are dealing with abstract enums and don't know the representation type.
1enum Status {Active, Paused, Off}2
3declare const status: Status;4const statusString: string = status.valueOf();
To convert from a nullable enum type to nullable representation type, you can do:
1enum Status {Active, Paused, Off}2declare const maybeStatus: ?Status;3
4// Using `as` cast and `&&`:5const maybeStatusString1: ?string = maybeStatus && (maybeStatus as string);6
7// Using `.valueOf()` and optional chaining:8const maybeStatusString2: ?string = maybeStatus?.valueOf();
If you want to convert from the representation type (e.g. string
) to an enum type (if valid), check out the cast method.
Methods
Enum declarations also define some helpful methods.
Below, TEnum
is the type of the enum (e.g. Status
), and TRepresentationType
is the type of the representation type for that enum (e.g. string
).
.cast
Type: cast(input: ?TRepresentationType): TEnum | void
The cast
method allows you to safely convert a primitive value, like a string
, to the enum type (if it is a valid value of the enum), and undefined
otherwise.
const data: string = getData();
const maybeStatus: Status | void = Status.cast(data);
if (maybeStatus != null) {
const status: Status = maybeStatus;
// do stuff with status
}
Set a default value in one line with the ??
operator:
const status: Status = Status.cast(data) ?? Status.Off;
The type of the argument of cast
depends on the type of enum. If it is a string enum, the type of the argument will be string
.
If it is a number enum, the type of the argument will be number
, and so on.
If you wish to cast a mixed
value, first use a typeof
refinement:
const data: mixed = ...;
if (typeof data === 'string') {
const maybeStatus: Status | void = Status.cast(data);
}
cast
uses this
(representing the object of enum members), so if you want to pass the function itself as a value, you should use an arrow function. For example:
const strings: Array<string> = ...;
// WRONG: const statuses: Array<?Status> = strings.map(Status.cast);
const statuses: Array<?Status> = strings.map((input) => Status.cast(input)); // Correct
Runtime cost: For mirrored string enums (e.g enum E {A, B}
), as the member names are the same as the values, the runtime cost is constant -
equivalent to calling .hasOwnProperty
. For other enums, a Map
is created on the first call, and subsequent calls simply call .has
on the cached map.
Thus the cost is amoritzed constant.
.isValid
Type: isValid(input: ?TRepresentationType): boolean
The isValid
method is like cast
, but simply returns a boolean: true
if the input supplied is a valid enum value, and false
if it is not.
const data: string = getData();
const isStatus: boolean = Status.isValid(data);
isValid
uses this
(representing the object of enum members), so if you want to pass the function itself as a value, you should use an arrow function. For example:
const strings: Array<string> = ...;
// WRONG: const statusStrings = strings.filter(Status.isValid);
const statusStrings = strings.filter((input) => Status.isValid(input)); // Correct
Runtime cost: The same as described under .cast
above.
.members
Type: members(): Iterator<TEnum>
The members
method returns an iterator (that is iterable) of all the enum members.
const buttons = [];
function getButtonForStatus(status: Status) { ... }
for (const status of Status.members()) {
buttons.push(getButtonForStatus(status));
}
The iteration order is guaranteed to be the same as the order of the members in the declaration.
The enum is not enumerable or iterable itself (e.g. a for-in/for-of loop over the enum will not iterate over its members), you have to use the .members()
method for that purpose.
You can convert the iterable into an Array
using: Array.from(Status.members())
.
You can make use of Array.from
's second argument to map over the values at
the same time you construct the array: e.g.
const buttonArray = Array.from(
Status.members(),
status => getButtonForStatus(status),
);
.getName
Type: getName(value: TEnum): string
The getName
method maps enum values to the string name of that value's enum member. When using number
/boolean
/symbol
enums,
this can be useful for debugging and for generating internal CRUD UIs. For example:
enum Status {
Active = 1,
Paused = 2,
Off = 3,
}
const status: Status = ...;
console.log(Status.getName(status));
// Will print a string, either "Active", "Paused", or "Off" depending on the value.
Runtime cost: The same as described under .cast
above. A single cached reverse map from enum value to enum name is used for .cast
, .isValid
, and .getName
.
The first call of any of those methods will create this cached map.
Exhaustively checking enums with a switch
When checking an enum value in a switch
statement, we enforce that you check against all possible enum members, and don’t include redundant cases.
This helps ensure you consider all possibilities when writing code that uses enums. It especially helps with refactoring when adding or removing members,
by pointing out the different places you need to update.
const status: Status = ...;
switch (status) { // Good, all members checked
case Status.Active:
break;
case Status.Paused:
break;
case Status.Off:
break;
}
You can use default
to match all members not checked so far:
switch (status) {
case Status.Active:
break;
default: // When `Status.Paused` or `Status.Off`
break;
}
You can check multiple enum members in one switch case:
switch (status) {
case Status.Active:
case Status.Paused:
break;
case Status.Off:
break;
}
You must match against all of the members of the enum (or supply a default
case):
1enum Status {2 Active = 1,3 Paused = 2,4 Off = 3,5}6const status: Status = Status.Active;7
8// Error: you haven't checked 'Status.Off' in the switch9switch (status) { 10 case Status.Active:11 break;12 case Status.Paused:13 break;14}
9:9-9:14: Incomplete exhaustive check: the member `Off` of enum `Status` [1] has not been considered in check of `status`. [invalid-exhaustive-check]
You can’t repeat cases (as this would be dead code!):
1enum Status {2 Active = 1,3 Paused = 2,4 Off = 3,5}6const status: Status = Status.Active;7
8switch (status) {9 case Status.Active:10 break;11 case Status.Paused:12 break;13 case Status.Off:14 break;15 case Status.Paused: // Error: you already checked for 'Status.Paused' 16 break;17}
15:8-15:20: Invalid exhaustive check: case checks for enum member `Paused` of `Status` [1], but member `Paused` was already checked at case [2]. [invalid-exhaustive-check]
A default
case is redundant if you’ve already matched all cases:
1enum Status {2 Active = 1,3 Paused = 2,4 Off = 3,5}6const status: Status = Status.Active;7
8switch (status) {9 case Status.Active:10 break;11 case Status.Paused:12 break;13 case Status.Off:14 break;15 default: // Error: you've already checked all cases, the 'default' is redundant 16 break; 17}18// The following is OK because the `default` covers the `Status.Off` case:19switch (status) {20 case Status.Active:21 break;22 case Status.Paused:23 break;24 default:25 break;26}
15:3-16:10: Invalid exhaustive check: default case checks for additional enum members of `Status` [1], but all of its members have already been checked. [invalid-exhaustive-check]
Except if you are switching over an enum with unknown members.
If you nest exhaustively checked switches inside exhaustively checked switches, and are returning from each branch, you must add a break;
after the nested switch:
switch (status) {
case Status.Active:
return 1;
case Status.Paused:
return 2;
case Status.Off:
switch (otherStatus) {
case Status.Active:
return 1;
case Status.Paused:
return 2;
case Status.Off:
return 3;
}
break;
}
Remember, you can add blocks to your switch cases. They are useful if you want to use local variables:
switch (status) {
case Status.Active: {
const x = f();
...
break;
}
case Status.Paused: {
const x = g();
...
break;
}
case Status.Off: {
const y = ...;
...
break;
}
}
If you didn't add blocks in this example, the two declarations of const x
would conflict and result in an error.
Enums are not checked exhaustively in if
statements or other contexts other than switch
statements.
Exhaustive checking with unknown members
If your enum has unknown members (specified with the ...
), e.g.
enum Status {
Active,
Paused,
Off,
...
}
Then a default
is always required when switching over the enum. The default
checks for "unknown" members you haven't explicitly listed.
switch (status) {
case Status.Active:
break;
case Status.Paused:
break;
case Status.Off:
break;
default:
// Checks for members not explicitly listed
}
You can use the require-explicit-enum-switch-cases
Flow Lint to require that all known members are explicitly listed as cases. For example:
1enum Status {2 Active = 1,3 Paused = 2,4 Off = 3,5}6const status: Status = Status.Active;7
8// flowlint-next-line require-explicit-enum-switch-cases:error9switch (status) { 10 case Status.Active:11 break;12 case Status.Paused:13 break;14 default:15 break;16}
9:9-9:14: Incomplete exhaustive check: the member `Off` of enum `Status` [1] has not been considered in check of `status`. The default case [2] does not check for the missing members as the `require-explicit-enum-switch-cases` lint has been enabled. [require-explicit-enum-switch-cases]
You can fix if by doing:
// flowlint-next-line require-explicit-enum-switch-cases:error
switch (status) {
case Status.Active:
break;
case Status.Paused:
break;
case Status.Off: // Added the missing `Status.Off` case
break;
default:
break;
}
The require-explicit-enum-switch-cases
lint is not one to enable globally, but rather on a per-switch
basis when you want the behavior.
With normal enums, for each switch
statement on it, you can either provide a default
or not, and thus decide if you want to require each case explicitly listed or not.
Similarly for Flow Enums with unknown members, you can also enable this lint on a per-switch basis.
The lint works for switches of regular Flow Enum types as well.
It in effect bans the usage of default
in that switch
statement, by requiring the explicit listing of all enum members as cases.
Mapping enums to other values
There are a variety of reasons you may want to map an enum value to another value, e.g. a label, icon, element, and so on.
With previous patterns, it was common to use object literals for this purpose, however with Flow Enums we prefer functions which contain a switch, which we can exhaustively check.
Instead of:
const STATUS_ICON: {[Status]: string} = {
[Status.Active]: 'green-checkmark',
[Status.Paused]: 'grey-pause',
[Status.Off]: 'red-x',
};
const icon = STATUS_ICON[status];
Which doesn't actually guarantee that we are mapping each Status
to some value, use:
function getStatusIcon(status: Status): string {
switch (status) {
case Status.Active:
return 'green-checkmark';
case Status.Paused:
return 'grey-pause';
case Status.Off:
return 'red-x';
}
}
const icon = getStatusIcon(status);
In the future if you add or remove an enum member, Flow will tell you to update the switch as well so it's always accurate.
If you actually want a dictionary which is not exhaustive, you can use a Map
:
const counts = new Map<Status, number>([
[Status.Active, 2],
[Status.Off, 5],
]);
const activeCount: Status | void = counts.get(Status.Active);
Flow Enums cannot be used as keys in object literals, as explained later on this page.
Enums in a union
If your enum value is in a union (e.g. ?Status
), first refine to only the enum type:
const status: ?Status = ...;
if (status != null) {
status as Status; // 'status' is refined to 'Status' at this point
switch (status) {
case Status.Active: break;
case Status.Paused: break;
case Status.Off: break;
}
}
If you want to refine to the enum value, you can use typeof
with the representation type, for example:
const val: Status | number = ...;
// 'Status' is a string enum
if (typeof val === 'string') {
val as Status; // 'val' is refined to 'Status' at this point
switch (val) {
case Status.Active: break;
case Status.Paused: break;
case Status.Off: break;
}
}
Exporting enums
An enum is a type and a value (like a class is). To export both the type and the value, export it like a you would a value:
1export enum Status {}
Or, as the default export (note: you must always specify an enum name, export default enum {}
is not allowed):
1export default enum Status {}
Using CommonJS:
1enum Status {}2module.exports = Status;
To export only its type, but not the value, you can do:
1enum Status {}2export type {Status};
Other functions within the file will still have access to the enum implementation.
Importing enums
If you have exported an enum like this:
1// status.js2export default enum Status {3 Active,4 Paused,5 Off,6}
You can import it as both a value and a type like this:
import Status from 'status';
const x: Status /* used as type */ = Status.Active /* used as value */;
If you only need to use the type, you can import it as a type:
import type Status from 'status';
function printStatus(status: Status) {
...
}
Using CommonJS:
const Status = require('status');
Abstract enums ≥0.234
You can write code that operates on Flow Enums in a generic way using two types: Enum<>
and EnumValue<>
, which accept any Flow Enum and its values.
EnumValue<>
EnumValue<>
accepts any Flow Enum value. Want to narrow it down to enum values with a specific representation type? Just add a type argument:
1enum Status {Active, Paused, Off}2
3Status.Active as EnumValue<>; // OK4Status.Active as EnumValue<string>; // OK - its a string enum5Status.Active as EnumValue<number>; // ERROR - not a number enum
5:1-5:13: Cannot cast `Status.Active` to `EnumValue` because string [1] is incompatible with number [2] in the enum's representation type. [incompatible-cast]
The new EnumRepresentationTypes
type represents all the valid representation types: string
, number
, etc.
Flow Enum values don’t implicitly coerce to their representation type, so we allow explicit conversion
with casts like Status.Active as string
. With EnumValue<>
you might not know the specific representation type,
so we now allow casts using .valueOf()
directly on the enum value. For example:
1function f(e: EnumValue<>) {2 const x: EnumRepresentationTypes = e.valueOf(); // OK3}
Enum<>
Enum<>
accepts any Flow Enum - the enum itself, rather than its value. You can optionally supply a type argument to
restrict it to enums with a certain enum value, which you can pair with the EnumValue
type above:
1enum Status {Active, Paused, Off}2
3Status as Enum<>; // OK4Status as Enum<Status>; // OK5Status as Enum<EnumValue<string>>; // OK
With the Enum<>
type you can use all the Flow Enum methods like .cast
and .members
.
This lets you craft code that generically handles Flow Enums, for example creating a full React selector component from only a Flow Enum:
Usage
enum Status {
Active,
Paused,
Off,
}
<Selector items={Status} callback={doStuff} />
Definition
1import * as React from 'react';2import {useCallback, useMemo, useState} from 'react';3
4component Selector<5 TEnumValue: EnumValue<string>,6>(7 items: Enum<TEnumValue>,8 callback: (TEnumValue) => void,9) {10 // Typing the `useState` selector with the enum value generic11 const [value, setValue] = useState<?TEnumValue>(null);12
13 const handleChange = useCallback((e: SyntheticInputEvent<>) => {14 // Using the `.cast` method15 const value = items.cast(e.target.value);16 if (value == null) { throw new Error("Invalid value"); }17 setValue(value);18 callback(value);19 }, [items, callback]);20
21 return <ul>22 {Array.from(items.members(), item => // Using the `.members()` method23 <li>24 <label>25 {// Using the `.getName` method:26 items.getName(item)27 }:28 <input29 type="radio"30 // Casting to the representation type using `.valueOf()`31 value={item.valueOf()}32 checked={item === value}33 onChange={handleChange} />34 </label>35 </li>36 )}37 </ul>;38}
When to not use enums
Enums are designed to cover many use cases and exhibit certain benefits. The design makes a variety of trade-offs to make this happen, and in certain situations, these trade-offs might not be right for you. In these cases, you can continue to use existing patterns to satisfy your use cases.
Distinct object keys
You can’t use enum members as distinct object keys.
The following pattern works because the types of LegacyStatus.Active
and LegacyStatus.Off
are different. One has the type 'Active'
and one has the type 'Off'
.
1const LegacyStatus = Object.freeze({2 Active: 'Active',3 Paused: 'Paused',4 Off: 'Off',5});6const o = {7 [LegacyStatus.Active]: "hi",8 [LegacyStatus.Off]: 1,9};10const x: string = o[LegacyStatus.Active]; // OK11const y: number = o[LegacyStatus.Off]; // OK12const z: boolean = o[LegacyStatus.Active]; // Error - as expected
12:20-12:41: Cannot assign `o[LegacyStatus.Active]` to `z` because string [1] is incompatible with boolean [2]. [incompatible-type]
We can’t use the same pattern with enums. All enum members have the same type, the enum type, so Flow can’t track the relationship between keys and values.
If you wish to map from an enum value to another value, you should use a function with an exhaustively-checked switch instead.
Disjoint object unions
A defining feature of enums is that unlike unions, each enum member does not form its own separate type. Every member has the same type, the enum type. This allows enum usage to be analyzed by Flow in a consistently fast way, however it means that in certain situations which require separate types, we can’t use enums. Consider the following union, following the disjoint object union pattern:
1type Action =2 | {type: 'Upload', data: string}3 | {type: 'Delete', id: number};
Each object type in the union has a single common field (type
) which is used to distinguish which object type we are dealing with.
We can’t use enum types for this field, because for this mechanism to work, the type of that field must be different in each member of the union, but enum members all have the same type.
In the future, we might add the ability for enums to encapsulate additional data besides a key and a primitive value - this would allow us to replace disjoint object unions.
Guaranteed inlining
Flow Enums are designed to allow for inlining (e.g. member values must be literals, enums are frozen), however the inlining itself needs to be part of the build system (whatever you use) rather than Flow itself.
While enum member access (e.g. Status.Active
) can be inlined (other than symbol enums which cannot be inlined due to the nature of symbols),
usage of its methods (e.g. Status.cast(x)
) cannot be inlined.