In TypeScript, enums have a few surprising limitations. In particular, it can be challenging to check whether or not a value is in an enum in a type-safe way.
In this article, I’m going to explore that problem with a few examples.
Hopefully you’re using TypeScript with the --strict
flag. If you’re not,
I’ll point out where the trouble with enums can be particularly pervasive and
lead to uncaught runtime errors.
A practical example
A common use for string enums is to represent a set of static values, such as a set of options selectable by the user.
For example, if we wanted to model a list of fruits to use in a select
element, we might model those values as a Fruits
enum. An enum easily
translates into an array of strings because we can call
Object.keys(Fruits)
on it. But what about when we want to convert a string
the user selected into our enum type? I have often seen something like the
following example used to accomplish that.
In the following TypeScript Playground example, the --strict
flag (and
therefore inherently the --noImplicitAny
flag) is enabled, meaning this code
will not compile:
enum Fruits {
Apple = 'APPLE',
Pomegranate = 'POMEGRANATE',
Persimmon = 'PERSIMMON'
}
const onFruitChanged = (value: string): void => {
const fruit = Fruits[value];
console.log(fruit);
}
view this example on typescript playground
In the above example, you should see the following compiler error:
Element implicitly has an 'any' type because expression of type 'string'
can't be used to index type 'typeof Fruits'. No index signature with a parameter
of type 'string' was found on type 'typeof Fruits'.(7053)
If you’re running without --strict
or with --noImplicitAny
disabled, the
code would instead compile, but the constant fruit
would be typed as Fruits
.
But wait, if value
isn’t in the enum, shouldn’t it be Fruits | undefined
?
What’s actually going on here?
Shouldn’t we be able to look up the enum value given a string value? After all,
if you were to console.log(Fruits)
you would see that TypeScript is just
generating a plain old object as a lookup table:
{
Apple: "APPLE",
Persimmon: "PERSIMMON",
Pomegranate: "POMEGRANATE"
}
So why is it that we can’t reference a property of the object (e.g.
Fruits[value]
) and get back a type of Fruits | undefined
?
Without the --noImplicitAny
flag we can reference a property of the object,
but we don’t get back Fruits | undefined
, we just get back Fruits
because
TypeScript coerces the any
silently. This opens us up to a runtime error
where our constant fruit
may in fact be undefined
.
We would be introducing the same potential runtime error if we solved this
problem using a cast – value as Fruits
– when making our assignment const
fruit = Fruits[value as Fruits];
. Disabling --noImplicitAny
just further
hides these types of casts, as well as the issues they introduce into
a codebase.
In strict mode, we’re forced to address this issue. That’s a good thing. But as I discovered, the solutions are less than ideal.
Let’s validate that the value is actually in the enum
One way to do this would be with a type predicate:
enum Fruits {
Apple = 'APPLE',
Pomegranate = 'POMEGRANATE',
Persimmon = 'PERSIMMON'
}
const isFruit = (maybeFruit: string): maybeFruit is keyof typeof Fruits => {
return Object.values(Fruits).indexOf(maybeFruit) !== -1;
}
const onFruitChanged = (value: string): void => {
const fruit: string | undefined = isFruit(value) ? Fruits[value] : undefined;
console.log(fruit);
}
view this example on typescript playground
This does achieve the desired result and give us type safety, but it’s a bit long winded.
It turns out that even without the explicit predicate function, TypeScript can
infer the same type if we use the Array.find
method to resolve a value from
the string enum:
enum Fruits {
Apple = 'APPLE',
Pomegranate = 'POMEGRANATE',
Persimmon = 'PERSIMMON'
}
const onFruitChanged = (value: string): void => {
const fruit: string | undefined = Object.values(Fruits).find(x => x === value);
console.log(fruit);
}
view this example on typescript playground
Another option since TypeScript 3.4
Since TypeScript 3.4, there is another language feature that may be preferable over enums in certain cases, especially if you want access to both the type and value for a set of static strings.
We can define our set of values as an array, and apply a const assertion:
const fruits = ['APPLE', 'POMEGRANATE', 'PERSIMMON'] as const;
type Fruits = typeof fruits[number];
const onFruitChanged = (value: string): void => {
const fruit: string | undefined = fruits.find(fruit => fruit === value);
console.log(fruit);
}
view this example on typescript playground
The const assertion ensures that when we derive our type Fruits
from the
array:
- The literal strings in the array (e.g. ‘APPLE’, …) will not be type
widened to
string
- The array becomes a readonly tuple
As opposed to enums, this has a few benefits:
- We don’t have to call
Object.keys(Fruits)
whenever we want the values - We can always reference the values as
fruits
and the type asFruits
- Our array of values can never be modified. It is now read-only by definition
In conclusion
Enums can be challenging to use if you frequently need to check for existence of a string or numeric value within them.
If you don’t care about the values are are using the enum to represent a set of distinct states, you might want to use union types instead.
If – like in our above example – you need both the type and the associated values, we saw a way to access them in a type-safe way. Alternately, since TypeScript 3.4, we have the option of using const assertions which make it a bit easier to access the enum values.