logo
Published on

You don't need enums

Authors
  • avatar
    Name
    Ramon Alejandro

Enums are available in both Flow and TypeScript. Codebases may also use other tools such as keymirror.

Issues with keymirror:

  • More verbose than using string literals.
  • No type definitions (unless you also install its types package).

In both type systems, using enums has several drawbacks.

One common use case for enums is to define a set of values that can be used in an API surface. Think of variants in a button component or the possible event types.

Consider an enum for a log level:

  1. Enum
enum LogLevel {
  INFO = "INFO",
  WARNING = "WARNING",
  ERROR = "ERROR",
}

Some other alternatives are:

  1. keyMirror
const LogLevel = keyMirror({
  INFO: null,
  WARNING: null,
  ERROR: null,
});
  1. Regular object
const LogLevel = Object.freeze({
  INFO: "INFO",
  WARNING: "WARNING",
  ERROR: "ERROR",
});

Object.freeze is used to get a const type back (ie: we get INFO instead of string).

  1. String literals + unions
type LogLevel = "INFO" | "WARNING" | "ERROR";

Things that options 1, 2, and 3 have in common:

  • The LogLevel symbol must be in scope to be used.
  • It won't automatically give you a type for the possible values. When the property is typed as a string we are missing an opportunity to use the type system to catch bugs.
  • Options 1 and 3 are prone to mismatches between keys and values.
import { LogLevel } from "./...";

// ❌ weak typing
const log = (logLevel: string) => ...

// somewhere else
log(LogLevel.INFO);
log('foo');

In order to improve this we would have to define a type for the possible values:

type LogLevelT = (typeof LogLevel)[keyof typeof LogLevel];

and use it everywhere:

import { LogLevel, LogLevelT } from "./...";

// ✅ correct typing
const log = (logLevel: LogLevelT) => ...

// now this is redundant because TypeScript won't let you pass anything else
log(LogLevel.INFO);

// now this will fail
log('foo');

But why do all those extra steps when we can just use string literals + unions? By using option 4 we get the following benefits:

  • The LogLevel type is only needed when typing the property but it does not have to be in scope on the call site.
  • We define the set of possible values only once in a single type (there is only one symbol to keep in mind).
  • It's less likely that the property will be typed as string because there is a type for it.
  • Autocomplete works as expected.

One additional benefit of using string literals + unions is that it's easier to iterate over the possible values. When needed we can express the set of values like so:

const LogLevels = [
    "INFO",
    "WARNING",
    "ERROR",
] as const;
type LogLevel = typeof LogLevels[number];

This prevents us from having to use Object.keys or Object.values which are prone to errors.

References