Using discriminated union types in TypeScript
Tags: typescript, architecture, codequality
Published: 2023-03-19 13:13:52

## The compile time vs runtime type dilemma

TypeScript is a typed language that compiles to JavaScript, which itself doesn't have types beyond [the primitives](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/typeof#description). So when a developer writes TypeScript, it then gets transpiled into JavaScript, which is then executed in the runtime (e.g. a browser, or Nodejs on the server side). The process of transpilation leverages the static type information to catch a whole category of potential bugs, but the resulting runtime code is still just typeless JavaScript.

This has some implications that surprise developers coming to TypeScript from runtime typed languages. For one, you cannot check the type of anything at runtime, beyond the primitives. In practice:

```typescript
// primitives work...
const myNumber = 1;
const myString = 'some string';

typeof myNumber; // -> "number"
typeof myString; // -> "string"

// ...but more sophisticated types don't
type MyType = {
  name: string;
  func: () => number;
}

const myValue: MyType = {
  name: 'some name for my type',
  func: () => 123
}

typeof myValue; // -> "object", nothing more specific than that
```
_[The same code in TypeScript playground](https://www.typescriptlang.org/play?#code/PTAEAcCcEsFtoC7QG4FMDOoDuB7SBrAOmICgBjHAO3QVFgE8A5AV1gCNVJQBeUARgDc5KjTr0AyghiUA5j1ABydDlipQNaTIVCSCeuFQ4AZmJbtOA0CFABaAHygARJVYdIj3fsMmGkzZet7Jw1oWQ8Sa2JCNmZaWDw1ZXAAC2gaaDIAQwRUABNQPQNMXKoFBE8DUABZegAVL3kAbxJQUEpM1QAudSlQmSFWo2ZKMm6ACgBKHgcXc0ghAF8SYWo4+gA1TIAbZlRumvrK3mbW9q7FZVU2jrUjPDECrwUAGhbQIZHxqe4HPgAmADMJCWFW8Yk2O1QATAQUcODYACtUGQEI5nm0cAhUrI6Al1AYyNAjBkCslMpRSdkgA)_

## The problem

So let's say we have two types, `Cat` and `Dog`, and an `Animal` type that's a union of both:

```typescript
// all animals in our case share some base attributes
type BaseAnimal = {
  name: string;
  isFluffy: boolean;
}

// cats meow
type Cat = BaseAnimal & {
  meow: () => string;
}

// dogs bark
type Dog = BaseAnimal & {
  bark: () => string;
}

type Animal = Cat | Dog;

const whiskers: Cat = {
  name: 'Whiskers',
  isFluffy: true,
  meow: () => 'MEOWWW!' // Whiskers meows loudly
}

const pupper: Dog = {
  name: 'Pupper',
  isFluffy: false,
  bark: () => 'awooo' // Pupper has a gentle awoo instead of a loud bark
}

console.log(whiskers.meow()) // -> "MEOWWW!"
console.log(pupper.bark()) // -> "awooo"
```

Now let's write some business logic for an `Animal` to make noise, regardless of whether it's a `Cat` or a `Dog`:

```typescript
const makeNoise = (animal: Animal): string => {
  // ???
}
```

TypeScript cannot really help you here, as seen in the screenshot.

![TypeScript not being able to work on the meow and bark methods](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/uhh0s9pawov80zqwyfqr.png)

Because of the compile time vs runtime behaviour of types, you cannot do something like this:

```typescript
const makeNoise = (animal: Animal): string => {
  // Doesn't work because the type doesn't exist at runtime,
  // typeof will just return 'object'
  if (typeof animal === 'Dog') {
    return animal.bark();
  }

  // ...
}
```

Instead you can do something like this, but TypeScript is not happy about it:

```typescript
const makeNoise = (animal: Animal): string => {
  if (typeof animal.bark === 'function') {
    return animal.bark()
  }

  if (typeof animal.meow === 'function') {
    return animal.meow()
  }

  return 'not implemented!'
}

console.log(makeNoise(whiskers)) // -> "MEOWWW!"
console.log(makeNoise(pupper)) // -> "awooo"
```

TypeScript's unhappiness comes from the fact that the `.meow()` and `.bark()` methods don't exist on all the variations of `Animal`. Here's what the errors look like:


![TypeScript unable to work with partially accurate information](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/3ni9sx1twskrcxerb0i1.png)

Even if you accepted this approach, what would happen if the complexity grows? Let's say we want to add a `Cat.purr()` and `Dog.growl()` method for the types, and add those to the `makeNoise` output too. The code would get harder to work with right away, with more complex `if` statements and more room for bugs to creep in.

_[The code so far can be played with in TypeScript playground](https://www.typescriptlang.org/play?#code/PTAEEMBtIg7BLAtlAzqetQHsCuAnUAY3BQFNQUALcPclLRcgIxPPABd294md3SUAKHYBPAA7kAQqwCCCZDAC8oAN6DQoWOEYAuClwwBzANzr0KAGKQcAMxsi9TLFkilwsUwF9BgkEQ5ojFgA7sLi5ADCHKDK0mRySFCgAGSqZkHBegAUAJQxAHz63LAmgt6+YAAmWIZoLHgA1mESoAAiNTGgcaQJCilpGvUN2XmKhSgGJV4+oi29ScpR7KAAPm01PoRYsBOgwZTwKA2keCh6S51qGhpauqAA5ADqB0cnKPcANGYah1a29nouDhSF9rqAMiMCg8ALIAUQA8o8kQBCe6gPzPQ7HU7g0ghNCQXCVSAiMqbba7MQ4MQSPB6dqGS7fTTaUh6e4ABWptM+zN+1jsDlANlQIOZQ0hYwe4GCziwaL8XJpJ1A1DQ4FAhlIsHYrggsqw6B2-HAlWwNggoEJODNQzJgi2OxcpAAdITDFl9li3i6Mrk8n4ALSFABEcMRKJDDopzrdNSyVOVeBdQ396LAwdAIZlcqj5ON4PAxwAclhDuRlFl3IlIHp5pAcnoJsVGVKrtd4BasrM8RbqwoUzQGjFFMp7jYcLBCOx4Nt7nl22CNLR2PhMP2oIPGrlmeUwZ3QN3wlg+-JNxkR2OJ1OZ3OF8ywSu13Aa768cEd2C99cn3hMPdYCwZYkDEVxGB1UhKlRe1HXoVw4w9ZASzLMhPRebEUByAMM1DcMkUeZEo1g2N3SyJDSFLcsE25E4sPTUBM2zA0sCjIA)._

## The solution – discriminated unions

The solution for making the runtime "aware" of our compile time types is surprisingly simple. All we need is a _tag_ for the types to be able to uniquely identify each variant, and _discriminate_ based on that information. In practice this means adding a property with a literal value that can be used to identify each variant both at compile time and at runtime. 

**Note about the naming.** The attribute can be called anything as long as the name is consistent between the variants. Here we use `_t`. Other commonly used names are `type`, `tag`, or `kind`, with or without an underscore. It could also be something more relevant to the domain that's being modelled, so in our case of animals we could use something like `species`.

```typescript
// cats meow
type Cat = BaseAnimal & {
  _t: 'cat', // <- the discriminator for cat
  meow: () => string;
}

// dogs bark
type Dog = BaseAnimal & {
  _t: 'dog', // <- the discriminator for dog
  bark: () => string;
}

type Animal = Cat | Dog;
```

Note that the value of `_t` needs to be unique for TypeScript to be able to uniquely identify each variant. Now the business logic becomes easier: all we need to do is check for the value of `_t`, and the TypeScript compiler knows to automatically _narrow_ the type based on it. For example, and `Animal` with `_t === 'dog'` can be narrowed from `Animal` to `Dog`.

Note also the `assertNever` helper. Here it's used to make sure that all the different variants are handled in the branches of the `switch` statement.

```typescript
const assertNever = (n: never): never => {
  throw new Error('Should never happen')
}

const makeNoise = (animal: Animal): string => {
  switch (animal._t) {
    case 'cat':
      return animal.meow();
    case 'dog':
      return animal.bark();
    default:
      return assertNever(animal);
  }
}
```

The following screenshot illustrates how the compiler is able to narrow down from the generic `Animal` to the specific `Cat` and `Dog` based on the code branch we're in.

![The compiler can narrow down the type based on the code branch](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/9w6krrbuw7z7nkdr6bc6.png)

_[The solution code as a whole can be found here](https://www.typescriptlang.org/play?#code/PTAEEMBtIg7BLAtlAzqetQHsCuAnUAY3BQFNQUALcPclLRcgIxPPABd294md3SUAKHYBPAA7kAQqwCCCZDAC8oAN6DQoWOEYAuClwwBzANzr0KAGKQcAMxsi9TLFkilwsUwF9BgkEQ5ojFgA7sLi5ADCHKDK0mRySFCgAGSqZgD67HoA5MTs2QA0oH4APAC0oOyU5AAm8CiE3IgYHFgENm3+7GZBwXoAFACUMQB8+tywJoLevmA1WIZoLHgA1mESoAAiCzGgcaQJCilpGpk584aFxWDlldWgdQ1NLeydHQQXZssrA8OKYygDJMvD5RBtDkllFF2KAAD5bBY+QhYWCA0DBSj1FakPAoPTQ3ZqU5ZUC5DiFMxaXSkgDqmJQ2NxFI09Sstnsei4OFIBR6pBCv1GpIAsgBRADyNKlAEJstdQHSsTjAvzgmhILgapARNMkSi0WIcGIJHg9NtDISMiTshdmZptKQcgAFI0mu2s6x2BygGyoHlfGg-UBDIXZcDBZxYOV+F3GnGgahocCgQykWDsVwQCNYdCo-jgGrYGwQUAanCF766wTI1EuUgAOg1hn6GKVuPrvSGwz8ZTGACIxZKZX3q-q642Fv1DXG8PXvl35b3QH3w5GR6O8xAUGQ8OwAHKkABu8eU-VgelgR5xgwvV4I-xOdzwIU0pGCoFFeGfeH62QAypQuCQIWl7HgQ1BxrA2SDFWNZosg2J7lg9TkKe7iJJAegQpAN7jEYQpEhQwTwOwhCUMG6EKPWmTDIRGjEGQpJ5NkOhmBoGi0Ow+CYJRUAdqqQymOx-iMTaCwsWx7GcdxcAYXOgaCZJNSkL6OCQFkkkcaQXF4Dx244vud79LxOFCaA3gzHB45Nv0CGkEhKEtvSjIoIM3ZgEuA4SlKNLSiOVmuBOzZ2Q5ZBTq617uaAnmrs4I5AA)._

## Practical example – async HTTP states

While the previous example was hopefully illustrative, we rarely write code about cats and dogs. A more useful and complete example could be the familiar situation in SPA applications fetching data: rendering the different states.

Our imaginary application starts with a clean state, then performs an asynchronous HTTP API call to fetch some data, and then renders the resulting success or error based on what happened with the request. Here's a minimal example:

```typescript
type Data = {
  items: string[];
};

type State =
  | { _t: "initial" }
  | { _t: "loading" }
  | { _t: "error"; err: Error }
  | { _t: "success"; data: Data };
```

As you can see, we can _enumerate_ the different known states using discriminated unions. This way, when we write the rendering logic, we don't need to do any "if data then do something with data" type of checking. Instead we can just check for the value of `_t`, and the compiler knows to narrow it down based on that. 

Note again that `_t` could be called anything else too, as long as the name is consistent between the variants. In this case another example of a sensible discriminator name could be `status`.

The example is in react, but could be done in any other rendering pattern as well.

```typescript
const renderer = (state: State) => {
    switch (state._t) {
      case "initial":
        return <p>Click button to start</p>;
      case "loading":
        return <p>Loading...</p>;
      case "error":
        return (
          <div>
            <h3>Oops, error happened!</h3>
            <p>{state.err.message}</p>
          </div>
        );
      case "success":
        return (
          <div>
            <h3>Here's your data:</h3>
            <ol>
              {state.data.items.map((i) => (
                <li key={i}>{i}</li>
              ))}
            </ol>
          </div>
        );
    }
  }
```

_Here's the full working example in CodeSandbox: https://codesandbox.io/s/typescript-discriminate-unions-in-async-state-rendering-l2jbdl?file=/src/App.tsx_

## Other use cases

I have found discriminated unions especially helpful in situations where the result of an operation needs to be categorized into a success of a failure on a high level (hint: the [Either monad](https://gcanti.github.io/fp-ts/modules/Either.ts.html) is a super useful pattern), and the different variants of successes and/or failures categorized even deeper. So think something like:

```typescript
type Failure = LogicFailure | DatabaseFailure;

type Success = SuccessWithData | SuccessWithoutData;

type Result = Failure | Success;
```

You can see how the discriminated union pattern helps us compose the larger data types out of smaller enumerated pieces, and build the logic around what happens when each small piece is handled. This is useful for representing expected different outcomes of functions instead of throwing errors, and [this is discussed more in a separate post of mine](https://dev.to/anttispitkanen/typescript-why-you-shouldnt-throw-errors-to-control-the-program-flow-3g91).

This is also very useful for the purpose of building observability into the different variants, for example categorizing what kinds of errors are encountered. More on that in a separate post later.