The trouble with TypeScript enums

Wil Hall

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 as Fruits
  • 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.