# TypeScript's Discriminated Unions With Real Use Cases

Discriminated Unions are a powerful TypeScript feature that can lead to some very robust code that scales well on large teams. However it is a rather underrated & under utilised pattern. I'm going to give a real world example for using discriminated unions with a Redux example. You could also apply this pattern to message handlers, mappers, and more.

## Reducers

In this example we will build a simple reducer for managing some settings in an application. In this case, the discriminated union helps:

- ensure our code addresses every possible action in the system
- gives us fantastic type hints inside the branches of our code
- prevents us from attempting to access the wrong payload attributes
 
Let's take a look at the code first, then I will show you examples of modifying this code and how it helps prevent errors.

```typescript
/*
Lets imagine we're creating a settings page. Here the user can:
  - toggle sound on/off
  - change the appearance (theme)
  - enable/disable noitifications
  - Set their username
 */

type SettingsState = {
  isSoundOn: boolean;
  areNotificationsEnabled: boolean;
  theme: 'DARK' | 'LIGHT' | 'OCEANIC';
  username: string;
};

type SetSoundAction = {
  type: 'SET_SOUND';
  payload: {
    isSoundOn: boolean;
  };
};

type SetThemeAction = {
  type: 'SET_THEME';
  payload: {
    themeName: SettingsState['theme'];
  };
};

type EnableNotificationAction = {
  type: 'ENABLE_NOTIFICATION';
};

type DisableNotificationAction = {
  type: 'DISABLE_NOTIFICATION';
};

type SetUsernameAction = {
  type: 'SET_USERNAME';
  payload: {
    username: string;
  };
};

type SettingsAction =
  | SetSoundAction
  | SetThemeAction
  | EnableNotificationAction
  | DisableNotificationAction
  | SetUsernameAction;

const initialState: SettingsState = {
  isSoundOn: false,
  areNotificationsEnabled: false,
  theme: 'LIGHT',
  username: '',
};

function settingsReducer(
  state = initialState,
  action: SettingsAction
): SettingsState {
  switch (action.type) {
    case 'SET_SOUND':
      return {
        ...state,
        isSoundOn: action.payload.isSoundOn,
      };
    case 'SET_THEME':
      return {
        ...state,
        theme: action.payload.themeName,
      };
    case 'ENABLE_NOTIFICATION':
      return {
        ...state,
        areNotificationsEnabled: true,
      };
    case 'DISABLE_NOTIFICATION':
      return {
        ...state,
        areNotificationsEnabled: false,
      };
    case 'SET_USERNAME':
      return {
        ...state,
        username: action.payload.username,
      };
    default:
      return isNeverAction(action.type, 'settingsReducer');
  }
}

function isNeverAction(action: never, reducer: string): never {
  throw new Error(`${reducer} received invalid action ${action}`)
}

```

Let's take a look at what happens if someone adds an action and forgets to write a reducer for it.

I'll add an action like this:

```typescript
// other actions above
type SetEmailPreferences = {
  type: 'SET_EMAIL_PREFERENCES';
  payload: {
    marketing: boolean;
    transactional: boolean;
  };
};

type SettingsAction =
  | SetSoundAction
  | SetThemeAction
  | EnableNotificationAction
  | DisableNotificationAction
  | SetUsernameAction
  | SetEmailPreferences;
```
Now if I try to compile I get the following error

```plaintext
src/discriminated_unions.ts:99:28 - error TS2345: Argument of type 'SetEmailPreferences' is not assignable to parameter of type 'never'.

99       return isNeverAction(action, 'settingsReducer');
                              ~~~~~~
```

And in my IDE this looks like


![Type error from never inside WebStorm](https://cdn.hashnode.com/res/hashnode/image/upload/v1621148029883/NdA2hmrhL.png)

Awesome! Let's take a quick look at that type hinting.


![WebStorm autocomplete only has marketing or transactional](https://cdn.hashnode.com/res/hashnode/image/upload/v1621148329797/2I2Z4nRUi.png)

TypeScript is able to tell WebStorm that the available properties for `action.payload` inside this code branch can _only_ be `marketing` or `transactional` because we're looking at a discriminated type. An action with the type value `SET_EMAIL_PREFERENCES` can only have a payload of type `{ transactional: boolean, marketing: boolean }`

## Anatomy of a Discriminated Union

The most important aspect of a discriminated union is a property that acts as what is called the "discriminator". This is the property the compiler will use to differentiate between the different possible types. In the above example the discriminator was `type`, but it could be called anything. Another common name for discriminator properties is `kind`.

The discriminated union works because the discriminator itself is a narrow type, the union of possible types.

If we were to inspect the value of the discriminator in the above example, it is:

![Internals of SettingsAction['type']](https://cdn.hashnode.com/res/hashnode/image/upload/v1621148924775/mmHRmoFiv.png)

This has happened because we used a literal member to type our discriminator in each of our action types. 

```typescript
type SetEmailPreferences = {
  type: 'SET_EMAIL_PREFERENCES'; // This is a literal member
  payload: {
    marketing: boolean;
    transactional: boolean;
  };
};
```

In the above example, `type` is a literal member because it is a _value_ in a _type definition_. This is the key to implementing discriminated unions.

## What about that never?

We used the `never` type to implement **exhaustive checking**. This means the code won't compile unless we've addressed every type with a specific case in the switch.

How does this work? Let's break down the `isNeverAction` function

```typescript
function isNeverAction(action: never, reducer: string): never {
  throw new Error(`${reducer} received invalid action ${action}`)
}
```

Never is a type that won't accept any value, and we've used it for our `action` parameter. Passing any type to a never will always produce a compile time error. For example

```typescript
const cantSetMe: never = '';
```

This won't compile because `''` is a string and we're trying to assign it to a never.

Also notice that `isNeverAction` also returns a never. This is the true return type for a function that can only throw. It doesn't return `void` because in fact it can never return at all.

In the code, we called the `isNeverAction` function inside the default case at the very end of the switch statement. However TypeScript knows that the expression in the switch can only be the values in the type `SettingsAction['type']`, so if you provide a case statement for each one, and return or break in that statement, then it cannot reach the default case.

However, if we do forget a case, then we pass `action.type` to the never, and TypeScript will complain.

So why do we wrap this up in a function that throws? Well, TypeScript checks only work at compile time. If this were say, a discriminated union based on types from a database or 3rd party API, then we could encounter unpredicted types at runtime. If this does happen, it means the application has corrupt data and is in an unknown state. We should throw an error and stop what we're doing. This will help us find and diagnose errors early.

### What exactly is a `never`?

The `never` type is what we would call a `bottom` type in Set theory. Huh? So a `type` is really the set of all possible values. For example, the type `string` is the set of every possible string ever.

So `type Foo = 'foo';` is a member of the set `string`. It's also a member of the set `string | number` as is `0`, or any number.

It's also true that every type is a member of the set `unknown`, which is _every_ possible value. We call `unknown` the top type, because it is a superset of all other types.

Well if `never` is the bottom type and `unknown` is the top, does that mean it is the opposite of unknown? Uh, _kind of_. `Unknown` is the superset of any type, but `never` is a subtype of every set. 

Yes, that means `string` contains `never`, as does `number`, but you cannot set a `never` to any value from `string` because `never` is an empty set. 

`never` is even a subtype of sets you may make, so `'FOO' | 'BAR' | 'SAUSAGE'` includes `never`, but `never` of course cannot include the set.

This means that any given set, in union with `never`, is equivalent to the set. `never` is the identity element of the `union` operation.

```ts
T | never =>T
// more concretely
'FOO' | 'BAR' | never => 'FOO' | 'BAR'
```

You'll recognise that a union with `never` is very similar to addition with `0`, i.e.

```
number + 0 => number
1234 + 0 => 1234
```

Yes, `0` is the identity element of the addition operation.

**TLDR:** Check your unions against never to implement exhaustive checking.

## Conclusion

Hopefully now you have a new tool up your sleeve to help you write reliable code. Yet again, TypeScript saves us precious time and effort by raising errors in our code at compile time rather than runtime. This pattern gives us a powerful way for TypeScript to yell at us when we make changes and forget something, especially in a large code base with several developers.

And a powerful use-case for the `never` type! Have you seen any other great usages of discriminated unions or never? Let us know in the comments.
