TypeScript Enum's vs Discriminated Unions
Tags: typescript, enum, union, variant
Published: 2022-12-12 17:11:28


---
title: TypeScript Enum's vs Discriminated Unions 
published: true
description: In this article I wanted to compare and contrast Enum'ss and Discriminated Unions in TypeScript and compare them to each other.
tags: TypeScript,Enum,Union,Variant
canonical_url: https://jessewarden.com/2022/12/typescript-enums-vs-discriminated-unions.html
cover_image: 
---

In this article I wanted to compare and contrast [Enum](https://www.typescriptlang.org/docs/handbook/enums.html)‘s and [Discriminated Unions](https://www.typescriptlang.org/docs/handbook/2/everyday-types.html#union-types) in [TypeScript](https://www.typescriptlang.org/) and compare them to each other. In my [Object Oriented Programming](https://www.techtarget.com/searchapparchitecture/definition/object-oriented-programming-OOP) days, we’d occasionally to never use Enums, whether they were native to the language or emulated. Nowadays, in my [Functional Programming](https://en.wikipedia.org/wiki/Functional_programming) groove, I use Discriminated Unions all the time, whether native to the language or emulated. TypeScript is interesting in that it supports both natively.

Update: I have a video version of this article.

{% youtube yRkB0Ix14JA %}

I’ve recently been ripped from doing Functional Programming in [Elm](https://elm-lang.org/) on the front-end and [ReScript](https://rescript-lang.org/) on the back-end in a Serverless Environment into Object Oriented Programming using Angular & TypeScript on the front-end and TypeScript on the back-end in a server full environment. It’s been quite a shock to go back to what I moved on from 7 years ago. Thankfully TypeScript has started to incorporate some Functional Programming constructs so I can keep my sanity (or at least try).

By the end of this post, you'll see why Unions are superior to Enums despite extra typing, and how they can help you in server and UI development.

Why Even Care?
--------------

Enum’s and Discriminated Unions are for a specific data modelling type: the Or. There are basically 3 types we programmers use to model data:

- **Atomics**: a single data type (e.g. `var firstName = "Jesse"`) “This variable equals this.”
- **Ands**: combining 2 types like Object or Class (e.g. `{ name: "Albus", age: 6 }`) “These 2 things together are my variable.” "A dog has a name **AND** an age."
- **Ors**: allowing 1 of a set of options (e.g. `Breed = Sheltie | Lab | Husky`) “My value is 1 of these 3 options”. "The Breed is a Sheltie **OR** a Lab **OR** a Husky."

Using these 3 options, we can model our program. In the case of name, it’s a string because names can be anything… and so can strings. Age, however, is a number, and while numbers are infinite, we’ll be dealing with something along the lines of 0 to 12. For Breed, we know all the breeds we’re handling. However, our program can only handle 3 of the 360+ breeds at first, so we’ll just use those 3. The proper data type is an Or; the breed of a dog is ONLY 1 breed; it can’t be both a Lab and a Husky at the same time; it’s either a Lab OR a Husky.

Before Enum
-----------

Back in my [ActionScript](https://en.wikipedia.org/wiki/ActionScript) days, we didn’t have Enum natively in the language, nor was it in JavaScript. We’d typically use a set of constants denoted by their all uppercase spelling:

```javascript
var SHELTIE = 0
var LAB = 1
var HUSKY = 2
if(dog.breed === SHELTIE) {
...
```


Once OOP started to influence our designs, we started attaching those constants to Objects:

```javascript
var Breed = {
  Sheltie: 0,
  Lab: 1,
  Husky: 2
}
```


Once we got native class supported, we started using static vars with helper methods, usually on a Singleton:

```javascript
class Breed {
  static Sheltie = 0
  static Lab = 1
  static Husky = 2

  #inst
  constructor() {
    throw new Error("You cannot instantiate this class, use getInstance instead.")
  }

  function getInstance() {
    if(this.#inst) {
      return this.#inst
    }
    this.#inst = new Breed()
    return this.#inst
}

if(dog.breed === Breed.getInstance().Sheltie) {
...
```


Once we got the `const` keyword, we started using that instead of `var` or `let`.

The switch Problem
------------------

The challenge, however, was always “forgetting” all the ones you had. Some people would create helper functions or methods, but the typical scenario was, you’d often want to check all of those possibilities. I say “all the possibilities”, but the great thing about defining these emulated Enums is we were saying “It can be only **one** of these possibilities”. We’d typically check that via a switch statement:

```javascript
switch(dog.breed) {
  case Sheltie:
    ...
  case Lab:
    ...
```


The problem with the above code is 2 things. First, we’re forgetting one; Husky. That always happened as the code base grew or changed. Writing tests for that didn’t fix it because you had to remember to add to the test, and if you had remembered, you wouldn’t had forgotten it in the code in the first place.

Second, there wasn’t any default to catch in case you forgot, or someone ninja added a new Breed. That was super problematic because ActionScript/JavaScript are dynamic languages, and there is no runtime enforcement here, nor a compiler to help you beforehand, just a bunch of unit tests going “[We green, Corbin Dallas](https://www.youtube.com/watch?v=OGfYLRCJZ1c)!”

The whole point of an Or is to answer “Is it this or that?”. Not is it this or that or OMG WHAT IS THIS, THROW! Not, “it’s this or that or… dude, what was that thing… did you know about that, I sure didn’t?”

Enter Typed Enum
----------------

Once we got typed Enum’s, our compiler started helping us. If we forgot one in the switch, she’d let us know. TypeScript, assuming you’ve got [strictNullChecks](https://www.typescriptlang.org/tsconfig#strictNullChecks) enabled, will do the same.

```typescript
enum Breed {
    Sheltie,
    Lab,
    Husky
}

type Dog = {
    name: string,
    age: number,
    breed: Breed
} 

const printDog = (dog:Dog):string => {
    switch(dog.breed) {
        case Sheltie:
            return `${dog.name} the sheltie, super sensitive.`
        case Lab:
            return `${dog.name} the lab, loves fetch.`
    }
}
```


Because the `switch` is missing `Husky`, the compiler will give you an error:

```bash
Function lacks ending return statement and return type does not include 'undefined'.
```


Horrible compiler error, I know. Another way to read that is “Your function says it returns string, not string OR undefined”. Since you are missing Husky, the switch statement falls through. Since there is no `default` at the bottom, it just assumes the function is returning `undefined`. You can’t do that because the function ONLY returns string. You have 2 choices; fix it by adding the missing enum, have the function either have a default that returns a string, or below the switch statement return the string… or change the function to return a Discriminated Union; a `string` OR `undefined`. That last one is horrible, though, because it defeats the purpose of using Enums and Discriminated Unions to ensure our code handles only the Ors we’ve specified.

While the compiler error is rife with implementation details, it at least gives you a hint you missed the Enum Husky.

Creating Discriminated Unions Without Defining Them 
----------------------------------------------------

You’ll notice the [TypeScript docs immediately](https://www.typescriptlang.org/docs/handbook/unions-and-intersections.html) start using Discriminated Unions as parameter type definitions or function return value definitions. I think it’s better to compare them to Enum’s first, but I guess they were trying to show how easy it is to use them without you having to define anything beforehand. So we’ll go with it. The `padLeft` function, instead of the padding argument being of type `any`:

```typescript
function padLeft(value: string, padding: any) {
```


They instead improve it; “padding isn’t anything, it’s technically a string OR a number”. I say “improve” here, but really they’re *narrowing* the types. All types are thought of as narrowing our program’s inputs and outputs, sure, but Enums and Discriminated Unions in particular narrow to “only a set of these things”. So the docs narrow padding’s type from any’s anything to **only** string **or** number:

```typescript
function padLeft(value: string, padding: string | number) {
```


Notice we didn’t have to define the Discriminated Union to use it; we just put a pipe in there between `string` and `number`. Same goes for return values. Now, you can define it if you want to:

```typescript
type PaddingType = string | number

function padLeft(value: string, padding: PaddingType) {
```


Can we do this with Enum’s? Let’s try creating a function that will convert our Enum’s to strings:

```typescript
enum Breed {
    Sheltie,
    Lab,
    Husky
}

const breedToString = (breed:Breed):string => {
        switch(breed) {
        case Breed.Sheltie:
            return 'sheltie'
        case Breed.Lab:
            return 'lab'
        case Breed.Husky:
            return 'husky'
    }
}
```


Now if you misspell it, or if your function forgets one of the Enum values, the compiler will yell at you in a good way. Notice the key difference, though? We had to define the Enum first to use it. Discriminated Unions can create one on the fly if you already have types you want to bring together in an Or, like we did with `string` and `number` above.

Discriminated Unions as Type Gatherers, Not Just Values
-------------------------------------------------------

Also notice, though, we’re using the primitives as the type. You can’t go:

```typescript
enum PaddingType {
  string,
  number
}
```


Since that’s defining the words string and number as an Enum value, not a type. That’s another huge difference; Discriminated Unions unify a type, and can use primitive types to do that, not just your own. Enum’s are typically a group of number or string **values**.

Defining Discriminated Unions as Or Types
-----------------------------------------

Let’s use them like we use Enum’s. We’ll copy our Breed example, except use a Discriminated Union instead. This’ll show how they work just like Enum’s do in regards to Or like data:

```typescript
type Breed
    = 'Sheltie'
    | 'Lab'
    | 'Husky'
```


Notice unlike Enum, we have to use quotes as a string. Now our switch:

```typescript
const printDog = (dog:Dog):string => {
    switch(dog.breed) {
        case 'Sheltie':
            return `${dog.name} the sheltie, super sensitive.`
        case 'Lab':
            return `${dog.name} the lab, loves fetch.`
        case 'Husky':
            return `${dog.name} the husky, loves sitting on snow.`
    }
}
```


Like Enum’s, if we forget one, or misspell it, the compiler will tell us with a compilation error.

Discriminated Union as Strings
------------------------------

In practice, though, it looks exactly like Enum, sans the quotes. Almost. Notice both our type ‘Sheltie’ and the use of it in the case statement, ‘Sheltie’ is the same. Notice in our Enum example, it’s actually `Breed.Sheltie`, not just `Sheltie`. You can make Enum work that way via destructuring it immediately:

```typescript
enum Breed {
    Sheltie,
    Lab,
    Husky
}
const { Sheltie, Lab, Husky } = Breed
```


If you’re curious why, Enum’s in TypeScript are compiled to Objects whereas Discriminated Unions that are simple strings are compiled to strings; there is nothing to destructure, it’s just a string.

Functional Programming languages call these “tagged unions”; meaning the string is just a tag; a label defining what it is. You’ll define a bunch of tags, in this case 3 of them, and “unify them” into a single type, a Breed.

Where Enum’s End and Unions Begin
---------------------------------

We’ve already shown how Enum’s are values, but Discriminated Unions can be both values and types. Let’s show you how Union’s can be more than just a tag, or a string as it were. It can be a completely different Object/Class. We’ll use something practical, like a return value from an HTTP library that wraps the `fetch` function in Node.js and makes it easier to use by emulating Elm’s HTTP library and only returning the 5 types that matter:

- the URL you put into fetch is bogus
- your fetch took too long and timed out
- we got some kind of networking error; either your disconnected from the internet, your Host file is mucked up, or something else networking related is wrong
- We got a response from the server, but it was an error code of 4xx – 5xx
- We got a successful response from the server, but we failed to parse the body

The above is super hard to simplify in Fetch, but let’s assume Axios, undici, node-fetch, and all the other JavaScript libraries joined forces to make it simpler to use. How would they model that using an Enum? Maybe something like this:

```typescript
enum FetchError {
  BadUrl,
  Timeout,
  NetworkError,
  BadStatus,
  BadBody
}
```


That’s kind of cool. Now, you never need try/catch with an async/await `fetch`, nor a `catch` with a `Promise`. You can just use a switch statement with only those 5, and the compiler will ensure you handled all 5. However, we’re missing some data here… note my whining code comments:

```typescript
switch(err) {
  case BadUrl:
    // ok, but... what was the bad url I used?
  case Timeout:
    // cool
  case NetworkError:
    // cool
  case BadStatus:
    // ok, but... what was the error code?
  case BadBody:
    // ok, but... what _was_ the body? Perhaps I can parse it a different way, or interpret it to get more information of what went wrong?
}
```


You can see the problem here. Enum’s are just values; meaning “BadUrl” is just a number or a string; it’s just an atomic value, just one thing. What we need is an And, either an Object or Class.

If we defined those as types, they’d look like the below (yes, you can use `interface` below or a class if you wish, I’m just using `type` to be consistent and from my FP background).

```typescript
type BadUrl = {
  url: string
}
type BadStatus = {
  code: number
}
type BadBody = {
  body: string | Buffer
}
```


Let’s go over each one:

- The `BadUrl` is an Object with 1 property, url. It’ll be the URL we called fetch with, and the URL fetch is whining isn’t a good URL, like `{url: 'moo cow 🐮' }`
- The `BadStatus` is an Object with 1 property, `code`. It’ll be any number between 400 and 599, whatever the HTTP server sends back to us.
- The `BadBody` is an Object with 1 property, `body`. It’s either a string or a binary `Buffer`; we’re not sure which, so we use a Discriminated Union in the Object to say “The body is either a string OR a Buffer”. Notice we didn’t define another Union here, we just did it inline using the single pipe.

However, the above isn’t enough and won’t work. TypeScript needs the same property name for all Objects to have a unique value so it can tell them apart from a type level. You get this as [instanceof](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/instanceof) using a class for example:

```typescript
class BadUrl {
  constructor(public url:string){}
}
class BadStatus {
  constructor(public code:number){}
}
```


You can then at runtime figure out those types by asserting:

```typescript
const getThing = (classThing:any):string => {
    if(classThing instanceof BadUrl) {
            return `The URL is invalid: ${classThing.url}`
    } else if(classThing instanceof BadStatus) {
        return `Server returned an error code: ${classThing.code}`
    }
    return 'Unknown error type.'
}
```


Again, though, no compiler help to know if you’ve handled all cases, the above is all at runtime, not compile time.

The common thing to do is give them a property name they all have with a unique value for each. Let’s enhance those Objects with `errorType`. For brevity, leaving out Timeout & NetworkError:

```typescript
type BadUrl = {
  errorType: 'bad url',
  url: string
}
type BadStatus = {
  errorType: 'bad status',
  code: number
}
type BadBody = {
  errorType: 'bad body',
  body: string | Buffer
}
```


K, understand our 3 Objects? Now, let’s unify it into a single type:

```typescript
type FetchError
  = BadUrl
  | Timeout
  | NetworkError
  | BadStatus
  | BadBody
```


Looks about the same as the Enum, though. What happens if we use it in a switch statement?

```typescript
const getErrorMessage = (httpError:FetchError):string => {
    switch(httpError.errorType) {
        case 'bad url':
            return `The URl is invalid: ${httpError.url}`
        case 'timeout':
            return 'Took too long.'
        case 'network error':
            return 'Some type of networking error.'
        case 'bad status':
            return `Server returned an error code: ${httpError.code}`
        case 'bad body':
            return `Failed to parse body server returned: ${httpError.body}`
    }
}
```


If you’re willing to do the work of adding an extra property to each Object to help identify it in a switch statement, you get the same exhaustive check features that you get with Enum, or basic string Discriminated Unions with 1 additional feature; the ability to use various data, confidently, depending on the type!

Notice if we have a `BadUrl`, we can confidently access it’s `url` property:

```typescript
case 'bad url':
    return `The URl is invalid: ${httpError.url}`
```


But, if it’s instead a `BadStatus`, we can instead access the `code` property:

```typescript
case 'bad status':
    return `Server returned an error code: ${httpError.code}`
```


If it were a `BadUrl` and you tried to access the `code` property, you’d get a compiler error:

```bash
Property 'code' does not exist on type 'BadUrl'.
```


You can do that at runtime using classes and instanceof, but using Discriminated Unions, the compiler can help you before you compile to ensure your code is correct.

UI Uses Too
-----------

This concept of switch statements checking every possible case, and the compiler ensuring you’ve included all of them, as well as ensuring the data associated with each case is correct can help make your user interfaces more correct as well. Using Or’s to model types in UI development has an extremely common use case there: what screen to show the user.

We UI developers only build a few screens. Some of those only show certain screens or components to the user based on the state of our application. The most common is when loading data. We’ve all seen the common Loading, Failed to Load, and Error screens. Regardless of framework/UI library, it’s often modeled like this:

```javascript
{
  isLoading: true,
  data: undefined,
  isError: false,
  error: undefined
}
```


If we get data successfully, the Object is now:

```javascript
{
  isLoading: false,
  data: ["our", "data"],
  isError: false,
  error: undefined
}
```


### Impossible States

However, there are 2 problems with this, one of which [I’ve covered before](https://jessewarden.com/2019/02/easier-asynchronous-state-modelling-in-react-redux-or-hooks.html).

The first is [impossible states](https://www.youtube.com/watch?v=IcgmSRJHu_8). If you don’t modify the Object correctly, or have some kind of bug, you can end up with this Object:

```javascript
{
  isLoading: false,
  data: ["our", "data"],
  isError: true,
  error: new Error("b00m 💣")
}
```


What does it mean to have both data successfully AND an error? The UI code may, or may not, handle that with some leniency, but probably will show either stale data or an error screen when successful. Either is madness.

### The Loading Bug

The 2nd problem is the missing waiting state. Many UI developers, myself included for over a decade, simply use those 3 states to model UI applications. However, they’re missing a 4th state which correctly models what actually happens in larger UI’s, called Waiting. The reason you don’t hear about it is your internet is super fast, the data is cached, or you just never notice.

But the bug is prevalent in many popular applications. Kris Jenkins [covers the bug for Twitter and Slack in his post](http://blog.jenkster.com/2016/06/how-elm-slays-a-ui-antipattern.html) describing why he built the [Remotedata library](https://package.elm-lang.org/packages/krisajenkins/remotedata/latest/) for Elm. I’m stealing his images here to showcase the bug. Here’s a Tweet of his, notice there are no retweets or likes:

![](https://jessewarden.com/wp-content/uploads/2022/12/twitter_zero-1024x799.png)And here’s his Slack showing no Direct Messages, which is also untrue (he has lots of Direct Messages in his Slack):

![](https://jessewarden.com/wp-content/uploads/2022/12/slack_zero.png)Both correct themselves after the data loads. UI developers will typically default to “Loading…” in both the UI and in their data. Whether React’s initial hook/constructor, Angular’s ngOnInit, or some type of “run this code once when the component first renders”, you’ll kick off some data fetch for the component. The user will see a loading screen while it loads. However, many UI developers, such as Twitter & Slack developers, only render 1 state. Yes, one: success. If it failed, they ignore it. If it loads forever, they ignore it.

Others will show success and error. Many don’t show a loading screen. For a Designer, it’s not as simple as designing loading screens or spinners for every part. You’re designing an experience around what the user is trying to accomplish, and you constantly battle between the extremes of “show all the things happening” and “only show what they need to know”. The developer may have to make many calls and the designer may not be aware of each of those discrete loading states. This is not straightforward and is hard. They then have to work with the developer on what they can/can’t do, what worked well vs. not with the user. It’s a challenging, constantly changing, process.

### The Waiting State

So just use an Enum or Discriminated Union to ensure the UI shows all 3 states, right?

Well… not quite. 2 issues with that. Let’s tackle the first since it’s been a problem in the UI industry for awhile. There is actually a 4th state we need. Kris Jenkins advocates for a 4th state, called “Not Asked Yet”, which I’ll call “Waiting”. This is separate from “Loading”. It means no data fetch has been kicked off yet.

Now most UI developers will just ensure all UI’s always start in a Loading state. However, as your UI grows, that gets harder to ensure. Additionally, some UI’s need to handle a retry, which is either the error state, or a 5th state. Sometimes UI’s will give the user an opportunity to cancel a long running network operation, which again is another possible state.

However, for brevity’s sake, Waiting is important not for your user, but you the developer, to know you forgot to kick off the fetch call somewhere, or perhaps the user or some other process has to kick it off. You can get really confused as a dev when you see a loading screen, but look in your browser’s network panel and there is fetch call made, or worse, 50 of them and you’re not sure which one belongs to your component. Instead, if your component is in a Waiting state, you know for a fact you did not kick off the loading. Much better place to be debugging from.

### Drawing UI’s Using Discriminated Unions

In TypeScript, the types would look like this:

```typescript
type Waiting = {
    state: 'waiting'
}
type Loading = {
    state: 'loading'
}
type Failure = {
    state: 'failure',
    error: Error
}
type Success = {
    state: 'success',
    data: Array<string>
}

type RemoteData
    = Waiting
    | Loading
    | Failure
    | Success
```


Then in your UI code, (showing React) you’d render in a switch statement to ensure you draw every possible case:

```react
function YourComponent(props:RemoteData) {
    switch(props.state) {
        case 'waiting':
            return (<div>Waiting.</div>)
        case 'loading':
            return (<div>
              <p>Loading...</p>
              <button onClick={cancel}>Cancel</button>
            </div>)
        case 'failure':
            return (<div>Failed: {props.error.message}</div>)
        case 'success':
            return (<div>{renderList(props.data)}</div>)
    }
}
```


You’d see one of these 4 views, waiting, loading, error, **or** success:

![](https://jessewarden.com/wp-content/uploads/2022/12/waiting.png)

![](https://jessewarden.com/wp-content/uploads/2022/12/loading-1024x583.png)

![](https://jessewarden.com/wp-content/uploads/2022/12/error-1024x583.png)

![](https://jessewarden.com/wp-content/uploads/2022/12/success-1024x623.png)



Again, unlike Enum, a Discriminated Union allows your switch statement case to utilize the data, confidently, on that particular value. Above that would be the error state utilizing the error message, and the success data utilizing the data. Additionally, we won’t get an impossible state because Discriminated Unions, like Enum’s, can only be in 1 value at a time.

Conclusions
-----------

As you can see, Discriminated Unions have a lot in common with Enums, and can replace them. Below is a table of the pro’s and con’s comparing them:

|      Enum Pros      |      Enum Cons      |            Union Pros           |            Union Cons           |
|:-------------------:|:-------------------:|:-------------------------------:|:-------------------------------:|
| Single native word  | No data association | Both string and data            | No single native word           |
| Exhaustive Checking | destructure values  | Exhaustive Checking             | Objects require common property |
|                     |                     | data association                |                                 |
|                     |                     | Can be used without defining    |                                 |
|                     |                     | Don’t have to destructure names |                                 |

As someone coming from Functional languages, it’s disappointing TypeScript requires you to make Unions as strings vs. the native word Enum’s get to use (e.g. ‘Sheltie’ vs `Sheltie`). For example, here’s what it’d look like in [Elm](https://elm-lang.org/) or [ReScript](https://rescript-lang.org/) (the Functional version of TypeScript with sounder, less forgiving types):

```elm
type Breed
  = Sheltie
  | Lab
  | Husky
```


Also, the common property is annoying, and while worth it for ensuring sound types, again, I’m spoiled in Elm/ReScript/Roc/any FP language that has Discriminated Unions/Tags/Variants. For example, here’s how you’d define our HTTP Errors in Elm:

```elm
type HttpError
    = BadUrl String
    | Timeout
    | NetworkError
    | BadStatus Int
    | BadBody String
```


That `Int` and `String` parts, it’s just an Object that has a number, or an Object that has a string. Here’s how we’d switch statement on it, called [pattern matching](https://guide.elm-lang.org/types/pattern_matching.html):

```elm
case httpError of
  BadUrl url ->
    "URL is invalid: " ++ url
  Timeout ->
    "Fetch took too long."
  NetworkError ->
    "Some unknown networking error."
  BadStatus code ->
    "Server sent back an error code: " ++ (String.fromInt code)
  BadBody body ->
    "Failed to parse the body the server sent back: " ++ body
```


You’ll notice they look like Enums, and the compiler/runtime can “tell them apart”. TypeScript doesn’t have this so you have to give it some kind of property that’s the same between all union types so the compiler can switch on it.

If you’re new to TypeScript, or come from other FP languages, you may be surprised to see TypeScript’s common use of strings as data types. The joke amongst ML language people when they see String as a type is “Why is this untyped?”. However, TypeScript has super powerful compiler guarantee’s with strings that CSS and UI developers utilize all over the place, and Discriminated Unions are just one example of that. If it really gives you heartburn, you can change those to Enum’s and use keyOf in the switch statement, but… that’s overkill for what the compiler is already giving you.

All in all, I think Discriminated Unions give you the powers of Enum with strong-typing, the with the added flexibility of including data with your Or types that is worth the extra typing.