There are many situations in programming where a piece of data may be one thing or another thing.
For example:
Imagine we have a server, and we want to know if a function is working correctly.
Although we may get either an error response or a success response from the server, the response is still that single piece of data. Its just the type of response that can differ.
For these situations we can use a union type to model the possible data types that may be present at any given time.
In this guide, I'll break down just exactly what Union Types are in Typescript, as well as how to use them.
A quick heads up: Union types are a little complex to grasp at first, so don't feel bad if this doesn't make total sense right away. The good news is this post is fairly short to give you a quick intro to this topic, and how to start using these right away.
However, if you still struggle to understand some of the concepts covered here, how to apply them, or just want a deeper dive into TypeScript, then check out my complete course on Typescript here.
It takes you from a complete beginner to building real-world applications and being able to get hired, so there's something for every level of TypeScript user.
Check it out above, or watch the first videos here for free.
With that out of the way, let's get into union types and how they work...
So let's start with the basics. Union types make it possible to choose one of many possibilities.
What do I mean? Well, let's look at an example.
If we want to put types A
, B
, and C
into a union, the resulting union type would be one that could be either A
or B
or C
.
However, Union types can never be more than one of their component types at any given time.
There are many different ways we can take advantage of unions in TypeScript. The most straightforward usage of a union is to use it as the return type of a function.
What like?
Well, we could have a function that searches for a user and then returns UserData
when the user is found, or returns undefined
otherwise.
The code might look something like this:
// use a pipe (|) to create a union
function search(name: string): UserData | undefined {}
Pretty simple so far, but here's a key thing to remember - There are no extra steps needed to return data when using a union.
UserData
will workundefined
will workAnother useful thing we can do with unions is limit the data that exists in a particular property.
For example
We can put different kinds of logging levels in a union and then use that union to ensure that the log level is always valid:
// create a union of different log levels
type LogLevel = "debug" | "error" | "info" | "trace" | "warn";
// use the union in an interface
interface Log {
level: LogLevel;
msg: string;
}
Now whenever we use the Log
interface, we will only be allowed to set the level
property to the ones listed in the LogLevel
union:
const hello: Log = {
level: "debug",
msg: "hello",
};
const whoops: Log = {
// ERROR: cannot assign "whoops" to LogLevel
level: "whoops",
msg: "hello",
};
Since we encoded the log levels into the type system, TypeScript is able to provide us with two things:
In the example above, Log works because it's part of our log level, while whoops is not.
So let's take this a step further.
In order to make working with union members easier, we can also create a tagged union. Basically, this works by making a union of interfaces that all share a single property called a discriminator.
The discriminator can then be used to either determine (or discriminate) which union member is being operated upon by using a switch
statement.
For example
To illustrate how a discriminated union works, we could imagine a job processing system. The system has workers which accept messages sent to them, and we want to ensure that:
Using discriminated unions like this, we can model all available messages using TypeScript's type system and fulfill both requirements:
// union of objects
type SystemMessage =
| { kind: "ABORT"; jobId: number; reason: string }
| { kind: "DELETE"; jobId: number }
| { kind: "RETRY"; jobId: number; attemptsRemaining: number }
| { kind: "SAVE"; jobId: number; data: object }
See how that works? The discriminator is the kind
property, which we can then switch
on in the workers:
function processMessage(msg: SystemMessage) {
switch (msg.kind) {
case "ABORT":
// the `reason` property is available here
console.log(`job #${msg.jobId} aborted: ${msg.reason}`);
break;
case "DELETE":
console.log(`job #${msg.jobId} deleted`);
break;
case "RETRY":
// the `attemptsRemaining` property is available here
console.log(`retrying job #${msg.jobId} (${msg.attemptsRemaining} retries remaining)`);
break;
case "SAVE":
// the `data` property is available here
console.log(`job #${msg.jobId} saved`);
break;
}
}
If you have more complex union members, you can even split them up into their own interfaces and then make a union of those interfaces.
This will have the same result as the previous code:
interface Abort {
kind: "ABORT";
jobId: number;
reason: string;
}
interface Delete {
kind: "DELETE";
jobId: number;
}
interface Retry {
kind: "RETRY";
jobId: number;
attemptsRemaining: number;
}
interface Save {
kind: "SAVE";
jobId: number;
data: object;
}
type SystemMessage = Abort | Delete | Retry | Save;
So let's break this down.
Since we used a switch
on the kind
discriminator, TypeScript will produce an error if we accidentally forget to include the discriminator property, or if we misspell it.
This means that if your union members are complex and you decide to split them into their own interfaces, now you don't have to worry about forgetting the discriminator because TypeScript won't allow usage in the switch
unless all members have the discriminator property.
Obviously this is a mile high view, but hopefully you now have a clearer idea of what union types are in TypeScript and how they work, and have some ideas of where you'll add them to your own code.
Don't skip these! Union types are definitely something to keep in your everyday toolbox of TypeScript code.
Learning how to properly use them will make your code easier to work with overall, while also increasing the reliability and maintainability of your code - simply because TypeScript will check the unions at compile time.
Pretty handy right?
And like I said up top. If you're struggling to grasp some of the concepts around union types, how to apply them, or just want a deeper dive into TypeScript, then check out my complete course on Typescript here.
Check it out above, or watch the first videos here for free.
And better still? When you join, you'll have direct access to me, other students, as well as full-time TypeScript devs in our private Discord channel - so you'll never be stuck!
If you've made it this far, you're clearly interested in TypeScript so definitely check out all of my TypeScript posts and content: