TypeScript Union Types: A Beginners Guide

Jayson Lennon
Jayson Lennon
hero image

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.

learn typescript

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...

What are Union Types in TypeScript?

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.

  • Returning the UserData will work
  • Returning undefined will work
  • While any other data type as a return value, will result in a TypeScript compiler error

Another 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:

  1. IDE autocompletion when we try to set the log level
  2. Compiler errors if we enter a log level that doesn't exist in the union

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.

Discriminated / Tagged Unions for easier Union management

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:

  1. We can only construct messages that are valid, and
  2. Workers can process all types of messages

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.

union 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.

typescript union types switches

What union types will you add to your own code?

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!

More from Zero To Mastery

Type Checking In TypeScript: A Beginners Guide preview
Type Checking In TypeScript: A Beginners Guide

What if you could catch errors at both compile and runtime? Thanks to TypeScript's type checking feature, you can! Learn how in this guide, with code examples.

5 Reasons Why You Should Learn C# preview
5 Reasons Why You Should Learn C#

It's been 23 years since C# went live, but it's still growing in popularity. Find out why (+ why you should learn this language ASAP to advance your career).

TypeScript Arrays: Beginners Guide With Code Examples preview
TypeScript Arrays: Beginners Guide With Code Examples

Looking to improve your TypeScript skills? Learn how to store and access multiple data types, with this beginner's guide to using arrays in TypeScript.