- Published on
You don't need enums
- Authors
- 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:
- Enum
enum LogLevel {
INFO = "INFO",
WARNING = "WARNING",
ERROR = "ERROR",
}
Some other alternatives are:
- keyMirror
const LogLevel = keyMirror({
INFO: null,
WARNING: null,
ERROR: null,
});
- 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
).
- 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.