loading...

Finally, an easy way to use TypeScript enums

cubiclebuddha profile image Cubicle Buddha ・3 min read

One of the best things that TypeScript provides is a standard enum type that simply doesn't exist in JavaScript. Yes, yes... there are thousands of enum libraries out there in Github with various implementations. But there's something wonderful about having a standard built into the language.

tl;dr: I'm going to show a cool library that makes interacting with enums a joyous experience.

So what's the problem?

Well, as great as TypeScript enums are... they've lead to some verbose or unreadable code.

Like, let's say you have an enum for food ordering:

export enum FoodPreference {
    vegan,
    vegetarian,
    meatEater
}

Beautiful. But now you need to write some presentation logic. Let's first show a simple but highly-unreadable way to write that presentation logic:

const presentationString = filter === FoodPreference.vegan
            ? 'Do you have a nut preference?'
            : filter === FoodPreference.vegetarian
            ? 'Which protein would you like?'
            : 'What type of meat would you like?';

So, just like how people have food preferences... some people love nested ternaries. I am not one of those people.

While I love the immutability benefits that come with const, there are serious problems with the "eye-scan-ability" of nested ternaries. Furthermore, I recently described how fall-through approaches like that example put the code at risk for when a new case has been added to the enum.

The more readable, more defensive, but sadly verbose option

So we could rewrite the above code using our defensive technique by using the assertUnreachable function I described in my other article:

function askFirstQuestion(pref: FoodPreference): string {
    switch(pref){
        case(FoodPreference.vegan): {
            return 'Do you have a nut preference?';
        }
        case(FoodPreference.vegetarian): {
            return 'Which protein would you like?';
        }
        case(FoodPreference.meatEater): {
            return 'What type of meat would you like?'
        }
        default: {
            assertUnreachable(pref)
        }
    }
}

Hmmm, that's still a lot of typing. I don't even like switch cases that much because of known issues like falling through. And although assertUnreachable eliminates a lot of the problems with switch cases, I still feel like we can do better.

Look up in the sky! It's ts-enum-util to the rescue!

Some wonderful person decided to make a library that would simplify many enum operations. Check out their readme for more information, but for the moment let's see how this ts-enum-util improves our code:

function createPresentationString(pref: FoodPreference): string {
    return $enum.mapValue(pref).with({
        [FoodPreference.vegan]: 'Do you have a nut preference?',
        [FoodPreference.vegetarian]: 'Which protein would you like?',
        [FoodPreference.meatEater]: 'What type of meat would you like?',
    })
}

Isn't that much clearer? It's certainly shorter.

So is this just about less code?

Wait, before you say that it's just syntactic sugar... let's show the TLC (tender, love, and care) that the library author added to their library.

Watch what happens when you add a new entry into the enum:

Isn't that cool? We get all of the benefits of assertUnreachable without the readability problems of nested ternaries or the verbosity (and other known issues) of switch statements.

Thoughts?

So I'm a big fan of this new approach. But I'm interested in hearing your thoughts so please reply on twitter or in the comments. Also, be sure to throw a Github star or two to Jeff Lau's ts-enum-util library.

Posted on by:

cubiclebuddha profile

Cubicle Buddha

@cubiclebuddha

TypeScript nut + head writer at CubicleBuddha.com (other loves are cats, my wife, comic books, and VGs)

Discussion

markdown guide
 

Why use a library when a simple type definition would do fine for the most part?

enum FoodPreference {
    vegan = 'vegan',
    vegetarian = 'vegetarian',
    omnivore = 'omnivore'
}

const foodPreferenceQuestion : {[preference in FoodPreference] : string } = {
    [FoodPreference.vegan] : 'Do you like nuts?',
    [FoodPreference.vegetarian] : "You're really a pescatarian aren't you?",
    [FoodPreference.omnivore] : 'You are the most picky omnivore in the history of ever I bet'
};

function getFoodPreferenceQuestion(preference : FoodPreference) {
    return foodPreferenceQuestion[preference];
}
 

Ik im super late, but you can use

const foodPreferenceQuestion: Record<FoodPreference, string> = {
    [FoodPreference.vegan] : 'Do you like nuts?',
    [FoodPreference.vegetarian] : "You're really a pescatarian aren't you?",
    [FoodPreference.omnivore] : 'You are the most picky omnivore in the history of ever I bet'
};
 

That's basically what ts-enum-util does for you behind the scenes automatically, but more. It infers the type of the value you are "visiting"/"mapping", including whether it can be possibly null or undefined, and forces you to handle null/undefined values as necessary. It also allows you to optionally provide a handler for "unexpected" values that occur at runtime, but were not part of the original compile-time enum definition. It uses unique symbols as keys for these special handlers to absolutely guarantee 0% chance of collision with legitimate enum values.

Then there's also the other side of ts-enum-util which gives you convenient access keys/values of the enum at runtime, mapping from key->value and value->key, custom type guards to verify/cast number/string values as enum values, etc.

 

Oh cool! That's awesome! Thanks for commenting even if it's late! Knowledge is welcome any time! :D

 

Update: You're right. That object approach also has exhaustiveness checking. I'm glad I learned something! I will say that the ts-enum-util library has many more features than what I mentioned.

Here's my original reply for posterity. Note: I was super duper wrong.


You lose the exhaustiveness checking. This code compiles and it shouldn't:

enum FoodPreference {
    vegan = 'vegan',
    vegetarian = 'vegetarian',
    omnivore = 'omnivore'
    somethingNew = 'this is the new thing that is not going to get caught'
}

const foodPreferenceQuestion : {[preference in FoodPreference] : string } = {
    [FoodPreference.vegan] : 'Do you like nuts?',
    [FoodPreference.vegetarian] : "You're really a pescatarian aren't you?",
    [FoodPreference.omnivore] : 'You are the most picky omnivore in the history of ever I bet'
};

function getFoodPreferenceQuestion(preference : FoodPreference) {
    return foodPreferenceQuestion[preference];
}

// Woops, I get undefined;
const iHopeThisIsDefined = getFoodPreference(FoodPreference.somethingNew);

If you use the library, the code will throw a compiler error saying that you forgot to handle the new case.

 

I updated my comment reply because the object approach does work. Although the library does have some other really cool features like easy ways to iterate over the enum.

That could be useful, though I haven't really needed enums in recent times so maybe there is something for that as well?

Either way, glad you learned something new! :D

To be fair, I had to look it up a bit as I've only used enums a handful of times on my TS projects so far so I was unfamiliar but I was pretty sure that this was an existing feature. Basically what I'm trying to say is that I too learned this. :P

Yea I recently had to iterate over an enum for a React project and its basically a giant headache. So when you need to do it it’s nice to have a library to reduce the headache haha.