typescript: enums vs. string literals

2021-10-09

 | 

~4 min read

 | 

630 words

Since learning Typescript, I’ve been a big fan of Enums. I love the explicitness of them! Why guess and use a string when you can know that you’re using the right one?

Well, Typescript continues to evolve and I’m starting to wonder if string literals aren’t just as good (or good enough?) without quite the same weight of an Enum.

As a commenter in this Stack Overflow conversation points out:

As of yet, I haven’t found a case where enum worked better, more clearly or more safely than a string literal type. One advantage of string literals is that you can leverage generics with pick/keyof. I don’t think you can do that with an enum.

With that preamble, how might we convert take advantage of string literals?

Replacing Enums with String Literals

enum Role {
  Standard = "Standard",
  Admin = "Admin",
}

type Standard = {
  role: Role.Standard
  name: string
  age: number
}

type Admin = {
  role: Role.Admin
  name: string
  securityLevel: "Normal" | "Elevated" | "High"
}

type Employee = Admin | Standard

Now, if we wanted to create an employee, we’d do something like this:

const employee: Employee = {
  role: Role.Standard,
  name: "Joe Lewis",
  age: 22,
}

How might we do a similar thing if we weren’t using Enums?

type Standard = {
  role: "Standard"
  name: string
  age: number
}

type Admin = {
  role: "Admin"
  name: string
  securityLevel: "Normal" | "Elevated" | "High"
}

type Employee = Admin | Standard

Now, assigning the employee, we no longer have the Role enum, however the Employee type knows that there are only two strings that are allowed for the role key:

const employee: Employee = {
  role: "Executive",
  name: "Jimmy Dean",
  title: "CEO",
}

This will not compile because we haven’t defined an executive employee yet.

We can fix this easily enough:

+ type Executive = {
+     role: "Executive"
+     name: string
+     title: "CEO" | "CTO" | "CMO" | "CPO"
+ }
- type Employee = Admin | Standard
+ type Employee = Admin | Standard | Executive

Function Signatures

One thing that I have not yet figured out how to replicate is the function signature.

When you have an enum, you can use that as the type of a function:

function doSomething(role: Role) {
  return Role.Admin ? "do x" : "do y"
}

However, if we no longer have that available and the allowable strings are inferred by the Type, what can you do?

One solution is:

function doSomething(employee: Pick<Employee, "role">) {
  return employee.role === "Admin" ? "do x" : "do y"
}

In most cases, this probably works perfectly. How often would I be operating on the strings without the actual employee handy? On the other hand - this a very different API. I now need to pass an object with a role key on it instead of a string. Thankfully, though, we do get strict typing:

doSomething({ role: "Admin" })
doSomething({ role: "Executive" })
doSomething({ role: "Supervisor" }) // doesn't compile

There is a better way, however! We can extract the type of the key role from the union type:

type Role = Employee["role"]

Here’s a typescript playground with all of it in one place.

Wrap Up

The simplicity of string literals is certainly enticing, and so far, with a little help from friends, I’m finding that it’s possible to get by without using enums!

All of this is leading me ot wonder if my affinity for enums is dated. It won’t be the first time I’ve changed my mind (nor will it be the last). Earlier this year I wrote about my shifting preferences with respect to types and interfaces.


Hi there and thanks for reading! My name's Stephen. I live in Chicago with my wife, Kate, and dog, Finn. Want more? See about and get in touch!