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 anenum
.
With that preamble, how might we convert take advantage of 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
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.
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!