TypeScript's Discriminated Unions and How I Began to Worry Less
Abhishek S
Posted on September 15, 2023
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
}
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'.
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;
};
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.
- When the type is
image
, you will have altText and lowResUrl - When the type is
video
, you will have altText, thumbnailUrl and autoplay - 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;
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'.
}
}
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.
Note: Discriminated unions are a TypeScript feature, not an HR issue!
Posted on September 15, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.