Photo by David Selbert from Pexels

TypeScript Enums vs. Flow Enums

George Zahariev
Flow
Published in
6 min readSep 13, 2021

--

The Flow team recently announced Flow Enums, a new language feature from Flow. If you’re familiar with TypeScript, you might be wondering how this feature compares to TypeScript’s enums. While they superficially look similar, they are quite different.

This is an overview of some of the differences between TypeScript and Flow Enums. For full details read the Flow Enums documentation.

Exhaustive checking in switch statements

Flow Enums are always exhaustively checked in switch statements. If you forget a member, Flow will error:

// Flow:
enum Status {
Active = 0,
Paused = 1,
Off = 2,
}
const x: Status = ...;// ERROR: Incomplete exhaustive check: the member `Off` of enum
// `Status` has not been considered in check of `status`
switch (x) {
case Status.Active: break;
case Status.Paused: break;
// We forgot to add `case: Status.Off:` here,
// resulting in error above
}

TypeScript enums are not always exhaustively checked in switch statements: TypeScript will not complain if you miss a case like in the above example.

Implicit coercion

Flow Enums do not allow for implicit coercion to the enum type. Instead, you can make an explicit type-safe cast using the .cast enum method, which returns undefined if the value is not valid:

// Flow:
enum Status {
Active = 0,
Paused = 1,
Off = 2,
}
const num: number = 1;
// ERROR: `number` is incompatible with `Status`:
const incorrect: Status = num;
// Safe casts using the `.cast` method:
const maybeStatus: Status | void = Status.cast(num);
// Can use ?? for default:
const status: Status = Status.cast(num) ?? Status.Off;

TypeScript enums can allow for implicit coercions, including unsafe ones:

// TypeScript:
enum Status {
Active = 0,
Paused = 1,
Off = 2,
}
const num: number = 1;
const x: Status = num; // Allowed in TypeScript
const y: Status = 42; // Allowed in TypeScript

Flow Enums also do not allow implicit coercions the other way, from enum type to representation type, to prevent logic bugs. You can still explicitly do so with a type cast:

// Flow:
enum Currency {
USD = 0,
CAD = 1,
GBP = 2,
}
declare function getExchangeRate(currency: Currency): number;
// Forgot to call `getExchangeRate` on `Currency.CAD`
// ERROR: `Currency` is not a `number`:
const balanceUSD = 100 * Currency.CAD;
// You can explicitly cast to number if you wish:
const x: number = (Currency.CAD: number);

TypeScript enums implicitly coerce to their underlying type:

// TypeScript:
enum Currency {
USD = 0,
CAD = 1,
GBP = 2,
}
// Forgot to call `getExchangeRate`:
const balanceUSD = 1000 * Currency.CAD; // Allowed in TypeScript
const x: number = Currency.CAD; // Allowed in TypeScript

Default member values

Flow Enums do not default number values. They require explicit number values specified for a number enum.

// Flow:
enum Status {
Active = 0, // Explicit number value required
Paused = 1,
Off = 2,
}

TypeScript enums default number values, when they are not supplied:

// TypeScript:
enum Status {
Active, // Has value 0
Paused, // Has value 1
Off, // Has value 2
}

Flow does not allow defaulting of number enums because if a member from the middle of such an enum is added or removed, all subsequent member values would be implicitly changed. This can be unsafe (e.g. serialization, logging, push safety). Requiring the user to be explicit about the renumbering makes them think about the consequences of doing so.

Since we do not default number values, a Flow Enum without values specified defaults to string values which mirror the member names. Since each member name is unique and does not depend on the order of the members, it does not have the same issues as described above.

// Flow:
enum Status {
Active, // Has value "Active"
Paused, // Has value "Paused"
Off, // Has value "Off"
}

Mirrored string enums also allow for more optimized runtime implementations of the .cast, .isValid, and .getName methods, since the name is equivalent to the value.

Extending the enum

Flow Enums cannot be extended or modified. Once they are defined, they act like frozen objects.

TypeScript can allow enums to be modified or extended. This can interact with TypeScript’s implicit default number values discussed above:

// TypeScript:
enum Status {
Active = 0,
Paused = 1,
Off = 2
}
enum Status {
// Due to TypeScript's default values, this is implicitly `0`:
Disabled,
}
console.log(Status.Disabled === Status.Active); //=> true

Reverse mapping

For any kind of Flow Enum, if you have an enum value and want to get the name of the member for that value (e.g. for debugging, CRUD UIs), you can use the .getName method:

// Flow:
enum Status {
Active = 0,
Paused = 1,
Off = 2,
}
console.log(Status.getName(Status.Off)); //=> "Off"enum StatusStr {
Active = "a",
Paused = "p",
Off = "o",
}
console.log(StatusStr.getName(StatusStr.Off)); //=> "Off"

TypeScript directly adds a reverse mapping to the enum object, but only for number enums (not for string enums):

// TypeScript:
enum Status {
Active = 0,
Paused = 1,
Off = 2,
}
console.log(Status[Status.Off]); //=> "Off"enum StatusStr {
Active = "a",
Paused = "p",
Off = "o",
}
// ERROR: TypeScript doesn't support this for string enums:
StatusStr[StatusStr.Off];

Iterating over enum members

Flow Enums provide a .members() method, which returns an iterator (like Map.prototype.values) of the enum’s members.

// Flow:
enum Status {
Active = 0,
Paused = 1,
Off = 2,
}
for (const status of Status.members()) {
const name = Status.getName(status);
console.log(name); //=> "Active", "Paused", "Off"
}

TypeScript enums do not have special functionality in this regard. You can use a for-in loop to go over the members, but for number enums this will result in both the names and the values:

// TypeScript:
enum Status {
Active = 0,
Paused = 1,
Off = 2
}
for (const status in Status) {
console.log(status); //=> "0", "1", "2", "Active", "Paused", "Off"
}

Symbol enums

Flow Enums support symbol enums. Symbol enum values can never conflict with any other value in your program, since they are symbols and therefore unique. This comes with the downside that they cannot be serialized or inlined, but this is a trade-off you can choose to make:

// Flow:
enum Status of symbol {
Active,
Paused,
Off,
}

TypeScript does not support symbol enums.

Definition restrictions

Flow Enums are more restrictive than TypeScript enums when it comes to their definition. The general philosophy of the Flow Enums design was to start off being restrictive, and wait to see actual use-cases and requests before expanding functionality. It is always easier to loosen up constraints than tighten them up.

Unlike TypeScript, Flow Enum members must:

  • All have the same type (no heterogeneous enums)
  • Be literals, if a value is specified (no arbitrary expressions)
  • Either have all of their values specified, or all defaulted
  • Have unique member values
  • Have their member names start with an uppercase letter (lowercase starting letters are reserved for the enum methods like .cast)

You can read more about defining Flow Enums.

Inlining

TypeScript provides “const enums”, which allow you to specify at the definition site that this enum will be inlined when using TypeScript’s supplied build system.

Flow Enums are designed to have properties that make inlining possible (e.g. member values must be literals, and the enum definition is frozen) other than for enum methods usage and symbol enums, which cannot be inlined by their nature. Rather than implement inlining in Flow, we leave the implementation of that to whatever build system you are using. We view enum inlining as an optimization performed by the build system rather than a feature of the language. Internally at Facebook we have implemented inlining for many cases of Flow Enums in our primary internal build system.

More

This was an overview of some of the differences between Flow Enums and TypeScript enums. Take a look at the full documentation to learn more about Flow Enums.

--

--