An alternative way of creating enumerations in Typescript

Volodymyr Yepishev - Aug 3 '22 - - Dev Community

(the final code of this article is available in the playground.)

Today we will take a look at enum, an awesome feature of typescript and also at some problem we can run into while using them and a way to overcome it.

Let's assume we're building some frontend application communicating with API that serves something that can make use of enums, let it be cards for example. There are 4 suits which can be represented by an enum:

enum SuitsEnum {
  Clubs = '♣️',
  Spades = '♠️',
  Diamonds = '♦️',
  Hearts = '♥️',
}
Enter fullscreen mode Exit fullscreen mode

And there's an api which serves a random card, which has a name and a suit:

interface Card {
  name: string;
  suit: SuitsEnum;
}
Enter fullscreen mode Exit fullscreen mode

Looks neat. Then we decide to write a unit test and not to invent test data, we decide to generate it by sending request to our API. We perform the request to imaginary api/the-only-card-i-need-is and get a random card:

{
    "name": "Ace", 
    "suit": "♠️"
}
Enter fullscreen mode Exit fullscreen mode

Good, let's put it into a variable in our test:

const card: Card ={
  name: 'Ace',
  suit: '♠️'
}
Enter fullscreen mode Exit fullscreen mode

As we try to do it, we immediately run into a problem is not assignable to type SuitsEnum, though it is the exact value which lies in SuitsEnum.Spades.

Not good, of course we could @ts-ignore it, force-case as unknown as SuitsEnum or substitute with SuitsEnum.Spades. Yet, all these are ugly solutions and workarounds.

What could be done then? We could turn enum into '♣️' | '♠️' | '♦️' | '♥️', but that would force us into losing some flexibility which enum provides.

Essentially the problem here is that enum combines both type and value. Yet, we could create an alternative to enum with values and type separated, which could be used to fit our card needs.

We will start with as const assertion to create an enumeration of card suits:

const SUITS = <const>{
  Clubs: '♣️',
  Spades: '♠️',
  Diamonds: '♦️',
  Hearts: '♥️',
};
Enter fullscreen mode Exit fullscreen mode

These are basically the value part of our enum. With them, we can build the type:

type Suit = (typeof SUITS)[keyof typeof SUITS];
Enter fullscreen mode Exit fullscreen mode

Now, anything typed with the new type Suit can consume values from SUITS object or plain strings which correspond to those values, so it's more flexible than enum in our case, check this out:

const hearts: Suit = SUITS.Hearts; // ok
const spades: Suit = '♠️'; // ok
Enter fullscreen mode Exit fullscreen mode

Which means we can use this new type for typing stuff coming from our API:

interface Card {
  name: string;
  suit: Suit;
}

const card: Card ={
    name: 'Ace',
    suit: '♠️'
}
Enter fullscreen mode Exit fullscreen mode

No dirty tricks, just language superpowers :)

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .