Everyday TypeScript: Enum
Welcome to the Enum lesson!
This lesson is shown as static text below. However, it's designed to be used interactively. Click the button below to start!
We've seen type unions and literal types. By using them together, we can create types that only allow specific literal values.
>
type HttpMethod = 'GET' | 'POST';const method: HttpMethod = 'GET';method;Result:
'GET'
>
type HttpMethod = 'GET' | 'POST';const method: HttpMethod = 'HEAD';method;Result:
type error: Type '"HEAD"' is not assignable to type 'HttpMethod'.
TypeScript also has enums, which give us another way to express types that only accept certain literal values. Enums have several unique properties, but the way this feature fits into the language makes them problematic in everyday use. Nevertheless, you'll likely see enums in the wild, so in this lesson, we'll cover how enums work. Then we'll discuss why unions are usually preferable.
"Enum" is short for "enumeration", which means "mentioning things one by one". It's pronounced "ee-noom" or "ee-num". An enum type lists out the possible values that it can hold, much like a union type does. Each possibility gets a name that we can reference outside of the type.
>
enum HttpMethod {Get = 'GET',Post = 'POST',}const method: HttpMethod = HttpMethod.Post;method;Result:
'POST'
Enums values aren't limited to strings. In fact, if we leave off the enum's values, TypeScript will automatically give them number values, starting with 0:
>
enum HttpMethod { Get, Post }const method: HttpMethod = HttpMethod.Get;method;Result:
0
>
enum HttpMethod { Get, Post }const method: HttpMethod = HttpMethod.Post;method;Result:
1
Enums have a surprising number of features. For example, we can give one of the members an explicit number value, and the rest of the members will automatically increase from there.
>
enum HttpMethod { Get = 10, Post }const method: HttpMethod = HttpMethod.Get;method;Result:
10
>
enum HttpMethod { Get = 10, Post }const method: HttpMethod = HttpMethod.Post;method;Result:
11
When referencing enum values, we're forced to use their symbolic names, like
HttpMethod.Postabove. Unlike union types, we're not allowed to directly use the literal value.>
enum HttpMethod {Get = 'GET',Post = 'POST',}const method: HttpMethod = 'POST';method;Result:
type error: Type '"POST"' is not assignable to type 'HttpMethod'.
That might seem like an artificial restriction. At runtime, the variable will hold
'POST', so why not let us assign that value? The reason is that enum member names serve as the "single source of truth" about which values are allowed, which is intended to help with long-term code maintenance. Let's examine the argument in favor of enums, then we'll see why they don't help as much as we'd like.Suppose that we eventually need to replace the string
'POST'with'post'. We change the enum's value to'post'and we're done! Other code in the system only references the enum member viaHttpMethod.Post, and that enum member still exists.Now imagine the same change with a type union instead of an enum. We define the union
'GET' | 'POST', then later we decide to change it to'get' | 'post'. Because we used a union type instead of a catch-allstringtype, any code that tries to use'GET'or'POST'as anHttpMethodis now a type error.>
type HttpMethod = 'GET' | 'POST';const method: HttpMethod = 'GET';method;Result:
'GET'
>
type HttpMethod = 'get' | 'POST';const method: HttpMethod = 'GET';method;Result:
type error: Type '"GET"' is not assignable to type 'HttpMethod'. Did you mean '"get"'?
This is the main practical difference between enums and unions. When an enum member's value changes, we don't have to change any other code. With a union, we might have to update the value in many places. This is a benefit of enums, but a very minor one. Types like these don't change very often, and it's easy to update union values by following the type errors. The code maintenance argument for enums isn't very strong.
The downside to enums comes from how they fit into the TypeScript language. TypeScript is supposed to be JavaScript, but with static type features added. If we remove all of the types from TypeScript code, what's left should be valid JavaScript code. The formal word used in the TypeScript documentation is "type-level extension": most TypeScript features are type-level extensions to JavaScript, but they don't affect the code's runtime behavior.
Here's a concrete example. We write this TypeScript code:
>
function add(x: number, y: number): number {return x + y;}add(1, 2);Result:
3
The compiler checks the code's types. Then it needs to generate JavaScript code. Fortunately, that step is easy: the compiler simply removes all of the type annotations. In this case, that means removing the
: numbers. What's left is perfectly legal JavaScript code.>
function add(x, y) {return x + y;}add(1, 2);Result:
Most TypeScript features works in this way. To get JavaScript code, the compiler simply removes the type annotations. Whatever's left is JavaScript.
Unfortunately, enums break this rule. If the compiler simply removed the enum types from our code examples above, we'd still be left with code that references
HttpMethod.Post. That's an error:HttpMethodandHttpMethod.Postwere parts of a type, so they should be removed when TypeScript generates JavaScript code. But how can we referenceHttpMethod.Postif the compiler deleted it?>
// This code is running as regular JavaScript, not TypeScript!const method = HttpMethod.Post;method;Result:
TypeScript's solution is to break its own rule in this case. When compiling an enum, the compiler adds extra JavaScript code that never existed in the original TypeScript code. There are very few TypeScript features like this. Each of these unusual features adds a confusing complication to the otherwise simple TypeScript compiler model.
We recommend avoiding enums for two reasons. First, enums break TypeScript's core "type-level extension" rule, complicating our mental model of the language. Second, unions can do the same thing as enums, allowing only a certain set of literal types, with no major drawbacks. However, it's still good to have a basic familiarity with enums because they're sometimes used in real-world code, especially in older TypeScript codebases.