Stating the problem
As typescript is growing and gaining popularity recently, more and more javascript developers appreciate type safety. The list of features Typescript provides is huge and might be overwhelming, so in this post, I will focus on one of them which is easy to grasp and has a neat practical impact.
Let’s start with an example. Imagine you are developing an application with many user roles. It is pretty common for an application to be consumed by different users, isn’t it? The exact roles are not really important here, but let’s say they are admin
, consumer
and guest
. In typescript, we can declare users holding those roles as follows:
type Admin = {}
type Consumer = {}
type Guest = {}
Now, let’s consider a set of attributes each user role has. Usually, they are email
, firstName
and lastName
or something like that. But, wait, Guest
users probably won’t have those (they are guests after all), so let’s just leave this type empty for now.
type Admin = {
firstName: string
lastName: string
email: string
}
type Consumer = {
firstName: string
lastName: string
email: string
}
type Guest = {}
The user of an application could only be of one role. The way to represent this through types is to use a union
type.
type User = Admin | Consumer | Guest
Admins are famous for their exclusive abilities, and in our application, they are able to invite consumers. Let’s add a field indicating how many invitations an admin could send.
type Admin = {
firstName: string
lastName: string
email: string
numberOfInvitesLeft: number // <-- added
}
To make things more interesting and closer to a real application, let’s add a property exclusive to a Consumer
type.
type Consumer = {
firstName: string
lastName: string
email: string
premium: boolean // <-- added
}
This is a very simple example, and in reality, users could have dozens of disparate properties, which considerably complicates the codebase when you need to access certain properties.
const doSomethingBasedOnRole = (user: User) => {
// how do you check here that user is really an admin
if (user) {
// ...and do something with the `numberOfInvitesLeft` property?
}
}
One option is to check on the existence of the property.
const doSomethingBasedOnRole = (user: User) => {
if (user && user.numberOfInvitesLeft) {
// safely access `numberOfInvitesLeft` property
}
}
But this is a tedious and not a scalable solution. And what to do when `numberOfInvitesLeft` becomes an optional property?
Introducing Discriminated Union Types
This is where discriminated union types come into play. We just need to put an additional field in every user type indicating the role.
type Admin = {
firstName: string
lastName: string
email: string
numberOfInvitesLeft: number
role: "admin" // <-- added
}
type Consumer = {
firstName: string
lastName: string
email: string
role: "consumer" // <-- added
}
type Guest = {
role: "guest" // <-- added
}
Notice how I am putting a specific string as a type; this is called string literal type. What this gives you is that now you can use native JS language operators, e.g., switch case
, if
, else
to discriminate on the role.
const user: Admin = {
firstName: "John",
lastName: "Smith",
email: "[email protected]",
numberOfInvitesLeft: 3,
role: "admin",
}
const doSomethingBasedOnRole = (user: User) => {
if (user.role === "admin") {
// now typescript knows that INSIDE of this block user is of type `Admin`
// now you can safely call `user.numberOfInvitesLeft` within this block
}
}
The same applies to a switch case statement.
// ...
const doSomethingBasedOnRole = (user: User) => {
switch (user.role) {
case "admin": {
// now typescript knows that INSIDE of this block user is of type `Admin`
// now you can safely call `user.numberOfInvitesLeft` within this block
}
case "consumer": {
// do something with a `Consumer` user
// if you try to call `user.numberOfInvitesLeft` here, TS compiler errors in
//
// "Property 'numberOfInvitesLeft' does not exist on type 'Consumer'."
//
}
}
}
The benefits of discriminated union types are apparent because the type checking is based on explicit role property and not on ad-hoc properties which might or might not be related to a specific user.