TypeScript's Discriminated Unions and How I Began to Worry Less
Tags: typescript, discriminatedunions, typesafety, programming
Published: 2023-09-15 14:25:57

If you’ve used Typescript in any real capacity, you’ve probably run into unions. Either defined in third party libraries or your own definitions. You can define a union with almost any combination or number of types. They’re pretty simple and generally look like this

```ts
type Stage = "empty" | "personalInfo" | "billingInfo";

function allowSubmit(stage: Stage) {
  // Do something
}
```

This ensures that only computationally supported values are passable.

```ts
allowSubmit("empty"); // OK
allowSubmit("inPayment"); // Error: Argument of type '"inPayment"' is not assignable to parameter of type 'Stage'.
```

This by itself made the code more robust. But what about more advanced cases? 

Consider the snippet below. It defines a type for attachments of media posts.

```ts
type PostAttachment = {
  type: string; // Can be "image", "video" or "audio"
  url: string;
  altText?: string;
  lowResUrl?: string;
  thumbnailUrl?: string;
  autoplay?: boolean;
};
```

This looks fine on the surface, but that’s a lot more optional properties than I would like. TypeScript will make you explicitly check for the existence of those properties. That's a lot more if statements. Or if you're lazy, a lot of  `as string` assertions (Officer! He's right here!). In short, it's a pain.

This is where discriminated unions come into play. Let’s break down the conditions for the properties.

1. When the type is `image`, you will have altText and lowResUrl
2. When the type is `video`, you will have altText, thumbnailUrl and autoplay
3. When the type is `audio`, you will have only autoplay

Now, let’s transform the above type to something more intuitive.

```ts
type Image = {
  type: 'image';
  url: string;
  altText: string;
  lowResUrl: string;
};

type Video = {
  type: 'video';
  url: string;
  altText: string;
  thumbnailUrl: string;
  autoplay: boolean;
};

type Audio = {
  type: 'audio';
  url: string;
  autoplay: boolean;
};

type PostAttachment = Image | Video | Audio;
```

That looks a lot cleaner doesn’t it? By anchoring to the type property, TypeScript can narrow down on what fields are available.

```ts
function processAttachments(attachment: PostAttachment) {
  if (attachment.type === 'image') {
    // TypeScript knows that 'altText' and 'lowResUrl' exist here
    console.log(attachment.altText, attachment.lowResUrl);
  } else if (attachment.type === 'video') {
    // TypeScript knows that 'altText', 'thumbnailUrl', and 'autoplay' exist here
    console.log(attachment.altText, attachment.thumbnailUrl, attachment.autoplay);
  } else {
    // TypeScript knows that 'autoplay' exists here
    console.log(attachment.autoplay);
    console.log(attachment.lowResUrl); // Error: Property 'lowResUrl' does not exist on type 'Audio'.
  }
}
```

I've kept the example pretty simple to keep the post short. But that should still give you an idea about how powerful these features are. Discriminated unions offer a robust way to handle different and often complex shapes of data, in a type-safe manner. It’s like a Swiss Army knife—versatile, efficient, and indispensable once you understand its uses. They've made my life easier, and I'm sure they'll do the same for you. But remember it’s a Swiss Army knife, not a golden hammer.


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/zwnih4edv0r3tq2qvt03.jpg)

> Note: Discriminated unions are a TypeScript feature, not an HR issue!