Discriminated Unions in TypeScript - Conditional Types

Discriminated Unions are a TypeScript feature that facilitates expressing a value that could be one out of multiple possible types.

These types have a common property (a literal type), the 'discriminant', which is used to tell the types apart.

I promise this will make more sense when you see an example.

Let's imagine we're building a chat application. In our chat app, there exist various types of messages. Every type of message has some common properties and some properties that are unique to its type.

A common pattern I would see for something like this would be:

type Message = {
  content: string;
  type: "text" | "image" | "video";
  caption?: string;
  duration?: number;
}

But this is not ideal, as we only need the duration for our "video" type and caption for our "image" type. And with this current typing, we can't enforce the conditional typings.

So, how do we enforce it?

This is a perfect scenario for using a Discriminated Union.

Let's start by defining our types:

type Message = TextMessage | ImageMessage | VideoMessage;

interface TextMessage {
  type: "text";
  content: string;
}

interface ImageMessage {
  type: "image";
  content: string; // a URL to an image
  caption: string;
}

interface VideoMessage {
  type: "video";
  content: string; // a URL to a video
  duration: number; // duration in seconds
}

Or to clean it up further since content is common to all:

type Message = {
  content: string;
} & (TextMessage | ImageMessage | VideoMessage);

type TextMessage = {
  type: "text"
}

type ImageMessage = {
  type: "image"
  caption: string
}

type VideoMessage = {
  type: "video"
  duration: number
}

In this scenario, type is the discriminant property to differentiate between each message type.

So now, if we tried to declare a type:

const message: Message = { content: "some-image.jpg", type: "image" };

We get the following type error:

Type '{ content: string; type: "image"; }' is not assignable to type 'Message'.
  Type '{ content: string; type: "image"; }' is not assignable to type '{ content: string; } & ImageMessage'.
    Property 'caption' is missing in type '{ content: string; type: "image"; }' but required in type 'ImageMessage'.

This correctly tells us that when we use the type as "image", we need the caption assigned.

So to correctly declare the message in this instance, we would write something like:

const message: Message = { content: "some-image.jpg", type: "image", caption: "This is some image" };

And our Type errors will disappear! 🎉

As you can see, it's pretty easy to create different dependent properties conditionally using this method.

If you play around with the different types, you'll see the requirements change depending on which of the type's you add.


Follow me on Twitter or connect on LinkedIn.

🚨 Want to make friends and learn from peers? You can join our free web developer community here. 🎉

TypeScript
Avatar for Niall Maher

Written by Niall Maher

Founder of Codú - The web developer community! I've worked in nearly every corner of technology businesses: Lead Developer, Software Architect, Product Manager, CTO, and now happily a Founder.

Loading

Fetching comments

Hey! 👋

Got something to say?

or to leave a comment.