---
title: The case for Discriminated Union Types with Typescript
teaser: 'Why Discriminated Union Types are an important part of modeling solutions
  with TypeScript.

  '
tags: union types,typescript,javascript,web
author: Alejandro Dustet
published_on: 2019-09-30
---

![Typescript](https://images.thoughtbot.com/blog-vellum-image-uploads/tTd0Q8hqSLm1cG2FY8fM_ts-super-small.png)

It is quite common when modeling a real-life problem to have constraints such as
having a user account with either a nickname or an email, an avatar or an emoji.
[TypeScript] gives us tools to model this kind of data.

## What options do we have?

```typescript
type Boss = {
  president?: Official;
  king?: Monarch;
}

type Official = {
  name: string,
  age: number,
}

type Monarch = {
  name: string,
  title: string,
}
```

This example adds two optional properties to describe that situation. Notice
that to display a Boss's name, we have to check for the presence of at least one
attribute. A `boss` object might have either a `president` or a `king`, but the
shape of the data does not fit that description. Having `n` optional properties
isn't the best way to describe the presence of at least one of them.

The `Boss` type aims to represent it can be either an official or a
monarch. Having both optional properties opens up to many invalid
states. Such as the possibility of having none of the attributes, or having them
all at the same time.

If we model a solution with multiple optional properties, navigating the
codebase would come with multiple null object checks.

```typescript
const bossName({ president, king }: Boss): string => {
  if(president) {
     return president.name;
  } else if(king) {
    return king.name;
  }
  // Still the type definitions allows a case where both properties might
  // be null so we must plan for this
  else {
    return "Apparently total anarchy";
  }
}
```

How might we avoid this mismatch between the state of the problem and our domain
modeling? Typescript allows us to combine different types using the
__union operator__(|). When used along with [type aliases] to define a new type
is known as [union types].

```typescript
type Boss = Official | Monarch;

const bossName = (boss: Boss): string => {
  return boss.name;
}
```

This works particularly well here since both types have the same property.

## Type guards 💂‍♀️

What about when we have to reach for other attributes? The check for
nullability becomes a [type assertion].

```typescript
const bossDescription = (boss: Boss): string => {
  if((boss as Official).age) {
    const official = (boss as Official);
    return `${official.name}, ${official.age} years old`;
  } else if((boss as Monarch).title) {
    const monarch = (boss as Monarch);
    return `${monarch.title}, ${monarch.name}`;
  } else {
    return 'The anarchy continues';
  }
}
```

The __as__ operator instructs the compiler to treat the variable to the left
*as* the type to the right. In this particular example, we are converting an
argument of type `Boss` into an `Official` first and later into a `Monarch`. One
thing to notice with this operand is that we are using it twice in the if block.
First, to assert that we are dealing with an `Official` and then to access the
`age` property in the object. Given that we *guarded* that branch feels
unnecessary to specify again that we are in the presence of an instance of an
object of type `Official`.

The final type assertion feels unnecessary since there should be only two
possibilities. Since we are telling the compiler to __trust__ us on this
one(by using type assertions), we need to keep guiding it after that.

### More type guards

There are other ways to check the type: one of them is using type predicates
with the [is] operand.

```typescript
const isMonarch(boss: Boss) boss is Monarch {
  return (boss as Monarch).title !== undefined;
}
```

This approach works, but it feels like we are doing the compiler's job, the
compiler is probably better at figuring this out than we are. As long as we
stick to annotating the types from our queries, using type guards feels more
robust.

### In you go

Another tool at our disposal is the [in operator]. We can assert the presence of
the property in our instance before using it.

```typescript
const bossDeescription = (boss: Boss): string => {
  if ("age" in boss) {
    return `${boss.name}, ${boss.age} years old`;
  }
  return `${boss.title}, ${boss.name}`;
}
```

Using the __in__ operator confirms that the properties we check are present and
non-optional. However, the opposite is true in both counts. After this
predicate, the remaining type either does *not* have the checked property, _or_
it is undefined.

One downside to this alternative is that the action of picking the right
property involves insider knowledge about the type. Using a property that was
added to describe the object itself, to differentiate two types is far from
ideal. There's gotta be a better way!

![joey](https://images.thoughtbot.com/blog-vellum-image-uploads/93FRg1oRD6Sf5hNamdRA_joey.gif)

## Introducing Discriminated Unions:

A solution is using a combination of [string literal types], [union types],
[type guards], and [type aliases]. To discriminate one type from the other in a
union, we add a `__typename` property for that goal. Since it's purpose is to
differentiate, the value should be a constant. In the following example we use a
[string literal or singleton] type:

```typescript
type Official = {
  __typename: 'Official',
  name: string,
  age: number,
}

type Monarch = {
  __typename: 'Monarch',
  name: string,
  title: string,
}

type Boss = Official | Monarch;

const bossDescription = (boss: Boss): string => {
  if (boss.__typename === 'Official') {
    return `${boss.name}, ${boss.age} years old`;
  }
  return `${boss.title}, ${boss.name}`;
}
```

Now there is a straightforward and self-composed way of distinguishing the types
on our aliased union. Without having to peek into the type definition and not
leaving us exposed to future side effects from valid changes.

## Conclusion

When we model a problem that has property *A* or property *B* Typescript has
an impressive array of options to solve the problem. Making sure we evaluate the
options is our part. By using the `in` operator, we are relying on attributes
added with a purpose far from differentiating this type from the others. When
using a random attribute from a type that was not added to make it unique, we
are violating the contract agreed upon by consuming it. The `as` operator
demands to import all of the type definitions just for making the assertion
adding unnecessary dependencies to our code.

Discriminated Unions combine more than one technique and create self-contained
types. Types that carry all the information to use them without worrying about
name collisions or unexpected changes.

[TypeScript]: https://www.typescriptlang.org/
[type aliases]: https://www.typescriptlang.org/docs/handbook/advanced-types.html#type-aliases
[union types]: https://www.typescriptlang.org/docs/handbook/advanced-types.html#union-types
[string literal types]: https://www.typescriptlang.org/docs/handbook/advanced-types.html#string-literal-types
[type guards]: https://www.typescriptlang.org/docs/handbook/advanced-types.html#type-guards-and-differentiating-types
[type assertion]: https://www.typescriptlang.org/docs/handbook/advanced-types.html#type-guards-and-type-assertions
[is]: https://www.typescriptlang.org/docs/handbook/advanced-types.html#using-type-predicates
[in operator]: https://www.typescriptlang.org/docs/handbook/advanced-types.html#using-the-in-operator
[string literal or singleton]: https://www.typescriptlang.org/docs/handbook/enums.html#union-enums-and-enum-member-types
