loading...

Using enums with string values in TypeScript? Consider string literals instead!

bholmesdev profile image Ben Holmes Updated on ・3 min read

If you've been using TypeScript for any amount of time, you've probably wondered this at least once:

Can I use string values instead of numbers on TypeScript enums?

This often comes up when you want a variable to have a select few string values. For example, say you're creating a banner for a website that's either yellow for warnings or red for emergencies. You want to make something reusable, so you add a enum for which type of banner it is:

enum BannerType = {
    Warning = "warning",
    Danger = "danger"
}

This gives you a lot of flexibility of how you can use that enum's value. A common use might be defining a class name for styling your banner:

{/* Yes, this is written a JSX-y fashion for you React users */}
<div className={BannerType.Danger}>Uh oh!</div>

This is much easier than writing weird helper functions and ternaries to figure out what class name to use. There are many more use cases for enum string values, like object keys, CMS content identifiers, paragraph text, error logs, etc etc etc.

Okay, I know string enums are useful. That's why I clicked on this!

Great! Now lets jump to the problems you can have with enums + string initializers:

  • They're a little verbose
  • They require lookups and tooltips to see what the actual string value is
  • They're limited in the special characters the enum can use

This last point was a huge point of friction for my web development team at Peloton. To explain, the team was looking to generate keys for content coming from the Contentful CMS. In Contentful, a key can be any string you could dream up. This means you can, say, include dots to indicate a subcategory (ex. "labels.danger") or dashes to mirror URL slugs (ex. "checkout-promo-code").

Clarification: A "CMS" is an external service to host all of the content for your website. In our case, we are using Contentful to store all of the header text, body text, images, and videos we display. In order to retrieve this content, we make an API call to fetch by specific keys.

This poses a problem for our enum solution. We need to use the keys in order to retrieve the site's content, and mapping each Contentful key to an enum means tossing out all the dots and dashes! Needless to say, this could lead to some nasty collisions between keys that are unique in Contentful but not unique on our hacky enums.

String literals to the rescue!

Luckily, TypeScript has a cleaner solution when you need those string values. Basically, you can provide a finite list of strings a variable can be assigned. Otherwise, it should throw a type error.

Example of assigning something invalid to a string literal type

This will also prevent you from assigning a traditional "string" type to the string literal. So, when declaring your types, you'll need to export the string literal type and use it the same way you would use an enum.

Example of autocomplete for string literals using VS Code

You can see from the informative gif above that autocomplete works as well!

Limitations

Admittedly, string literals aren't the silver bullet for every situation. Notably, using string literals doesn't improve on the verbose nature of enums. In fact, it'll often provide more information than necessary when assigning the literal type.

It's also more visually unclear what all possible values are when assigning 'random string' instead of SpecificTypes.Enum. This requires team communication to decide if string literals work best for smooth PR reviewing and text editor / IDE support.

Thanks for reading!

I'm a frontend webdev and designer always tinkering with something. I just snagged a shiny "dot dev" URL for my portfolio, so head over there for all my socials and a little more about my dev journey!

👉 https://bholmes.dev

Posted on by:

bholmesdev profile

Ben Holmes

@bholmesdev

GA Tech grad and full stack web dev all about good design, good music, and good code

Discussion

markdown guide
 

I would add that a benefit of string literal types is that they don't emit anything.

As opposed to a simple enum like this:

enum BannerType {
    Warning,
    Danger
}

resulting in this:

var BannerType;
(function (BannerType) {
    BannerType[BannerType["Warning"] = 0] = "Warning";
    BannerType[BannerType["Danger"] = 1] = "Danger";
})(BannerType || (BannerType = {}));

I like TypeScript when it's just type annotations, so I don't need to guess what's happening at runtime.

Here's an example in the TS Playground

 

You can use const enum if you don't like that behavior.

 

Yes, but I don't think they work when using the --isolatedModules compiler option.

I mean they work but only within a module. You can't export a const enum.

If you have types imports and exports, most of the type checking won't work when you use isolated modules.