TypeScript's Discriminated Unions and How I Began to Worry Less

abhishekvash

Abhishek S

Posted on September 15, 2023

TypeScript's Discriminated Unions and How I Began to Worry Less

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

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

function allowSubmit(stage: Stage) {
  // Do something
}
Enter fullscreen mode Exit fullscreen mode

This ensures that only computationally supported values are passable.

allowSubmit("empty"); // OK
allowSubmit("inPayment"); // Error: Argument of type '"inPayment"' is not assignable to parameter of type 'Stage'.
Enter fullscreen mode Exit fullscreen mode

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.

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

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.

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;
Enter fullscreen mode Exit fullscreen mode

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

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'.
  }
}
Enter fullscreen mode Exit fullscreen mode

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

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

💖 💪 🙅 🚩
abhishekvash
Abhishek S

Posted on September 15, 2023

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related