---
title: The trouble with TypeScript enums
teaser: Enums are perfect for modeling a set of options. Or are they?
tags: typescript,enum,union types
author: Wil Hall
published_on: 2020-03-09
---

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.

[with the `--strict` flag]: https://www.typescriptlang.org/docs/handbook/compiler-options.html#compiler-options

## 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:

```ts
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](https://www.typescriptlang.org/play/index.html#code/KYOwrgtgBAYgTmAlgFwM5QN4Cgq6gQQAdCAbYKAXigHJ8AFOgGQFFqAaHPOgewmAHM4AQxBDk5KtToB5ALLMA4gCV8AOXwAVVhzxQ6wOKkQQI3EJRp1mSgMoBJWbOmrqWAL5YsAYzOpkUM3gkZABhAAsRfmAAEwsACgA3IRIwYAAuKD84RBB+AEoMhO5EWIoAPkwoTlwfED8oADMEFAsglFQAbSSU4ABdAG5qqFrUbjIAOhJufjim4LzBtyA)

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:

```json
{
    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:

```ts
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](https://www.typescriptlang.org/play/index.html?ssl=1&ssc=1&pln=2&pc=22#code/KYOwrgtgBAYgTmAlgFwM5QN4Cgq6gQQAdCAbYKAXigHJ8AFOgGQFFqAaHPOgewmAHM4AQxBDk5KtToB5ALLMA4gCV8AOXwAVVhzxQ6wOKkQQI3EJRp1mSgMoBJWbOmrqWAL5YsAYzOpkURFR4JH8qAAoIIQBPACNgYJQALig-OEQQfgBKZMjY+IQUAPQAa2Ao7gAzKGQowmBK2AK0SgA+TE5cOGBkMDhzaRiAK2AvZAA6ADchEjBgVDCEtEyx9IATYAAPaQqI6LjFzKgAQgoqAFoARgBud08fED8oM0WAYQALEX5gVYswqZngMlUuksskJtxED8KG0MFAOlB7o8Kk0gcg0hkoAAfKBgEDrCrpb4WQKLP7TWaHAD8jRCqAA2v9ZgBdKDJXH4wmrG66RHcMhjEjcfhhZEhTI3NxAA)

[type predicate]: https://www.typescriptlang.org/docs/handbook/advanced-types.html#using-type-predicates

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:

```ts
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](https://www.typescriptlang.org/play/index.html#code/KYOwrgtgBAYgTmAlgFwM5QN4Cgq6gQQAdCAbYKAXigHJ8AFOgGQFFqAaHPOgewmAHM4AQxBDk5KtToB5ALLMA4gCV8AOXwAVVhzxQ6wOKkQQI3EJRp1mSgMoBJWbOmrqWAL5YsAYzOpkUM3gkZABhAAsRfmAAEwsACgA3IRIwYAAuKD84RBB+AEoMhO5EWIoAPkwoTlwfED8oADMEFAysnP4oAB8oMBBo4AacmItpACMAK2AvZAA6JJTgVDiglFQ8mcG+uIAPSgrdikOoedS8gG5qqFrUbjIZkm5+OKbg8-cgA)

## 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]
(that's the `as const` at the end of the assignment below):

```ts
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](https://www.typescriptlang.org/play/?ssl=1&ssc=1&pln=7&pc=2#code/MYewdgzgLgBAZgJwK4EsoRgXhgbQOQCCACkQDICieANDHkQPICy5A4gEoEByBAKpTXXJsAygElGjepzwBdGAEMMoSFADcAWABQUAJ4AHAKYwAYsjQZsuwyDjwz6HGCQBbAEYGEMjZq3LoMcFNUKABhAAt5MABzAwATLBgACgA3eQAbJAMALhhoBBRogEoc5JAUeMwAPhgAbxgtGEaYP1hEYJy8gqiYAB8YJDBYgzgCuIS28wA6EcHEidgqu2CsTGxUjINC7ybm8AgQNINJtJAoufstrQBfIA)

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

[const assertion]: https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-4.html#const-assertions
[readonly tuple]: https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-4.html#readonly-tuples

## 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.

[union types]:
https://thoughtbot.com/blog/the-case-for-discriminated-union-types-with-typescript
