Discriminated Union Types
This lesson introduces the concept of discriminated union types.
We'll cover the following
Overview
Now that you have a full understanding of union types, let’s look at one of their special categories.
Creating discriminated unions is an extremely powerful way of composing types. Thanks to this mechanism you can write code that is type-safe beyond your imagination. Discriminated unions let you enforce some business logic rules at compile-time.
Representing choice with optional properties
Let’s start by looking at an example. Imagine implementing an e-commerce application. When the customer registers on the app, they need to provide some contact details. One of the acceptance criteria says:
Customer needs to provide either email or phone number.
The most straightforward way of defining the Customer
type that satisfies this requirement is by using two optional properties.
interface Customer {
name: string;
email?: string;
phone?: number;
}
This solution has one major drawback. It’s possible to create a Customer
with neither email
nor phone
provided. In other words, it’s possible to create a Customer
object that violates the business requirement.
You might argue that the type will be accompanied by a validation function that will throw an exception if the object doesn’t have any of these two properties defined. This is slightly better, but still far from ideal because of the following:
- You will learn that the object is invalid at runtime
- When using
strictNullChecks
, you’ll need to writeif
statements every time you want to accessemail
orphone
- You might forget to call the validation function
Discriminated unions to the rescue
Instead of having two optional properties in Customer
type, let’s create a new Contact
type that is a union of two interfaces. One union member has a non-optional email
property and the other has a non-optional phone
property.
These two union members follow a special convention. They both have a string literal property called kind
. The type of that property is different in both members. This property is called a discriminator. It encodes type information into the object so that it is available at runtime. Thanks to this, TypeScript can figure out which union member you’re dealing with by looking at the kind
property.
interface EmailContact {kind: 'email';email: string;}interface PhoneContact {kind: 'phone';phone: number;}type Contact = EmailContact | PhoneContact;interface Customer {name: string;contact: Contact;}function printCustomerContact({ contact }: Customer) {if (contact.kind === 'email') {// Type of `contact` is `EmailContact`!console.log(contact.email);} else {// Type of `contact` is `PhoneContact`!console.log(contact.phone);}}
Here comes the power of discriminated unions. TypeScript analyses the code and sees an if
statement that checks whether contact.kind
is equal to "email"
. If that’s true, it can be certain that the type of contact
is EmailContact
, so it narrows the type inside the first if
branch. The only other option for contact
is to be a PhoneContact
, so the type is narrowed to PhoneContact
in the second if
branch.
You might be worried about the usage of a magic string in the if
statement. In fact, the string is not really magic. The type of contact.kind
(outside of the if
) is 'email' | 'phone'
. Try it yourself and see how the code editor suggests that the only possible values for contact.kind
are 'email'
or 'phone'
.
We’ve made an illegal state unrepresentable. In the previous solution, it was possible to create a customer without phone or email (i.e., it was possible to create an illegal piece of application state). Now it’s forbidden at compile time!
Note on formatting
Contact
type can be written in a much terser way as it’s not necessary to create standalone PhoneContact
and EmailContact
types - they can be inlined.
type Contact =
| { kind: 'email', email: string }
| { kind: 'phone', phone: number };
The first |
has been added only for formatting purposes. It’s allowed by TypeScript syntax and is a common convention to use when defining multi-line unions.
The next lesson dives deeper into using discriminated unions.