Everything You Need To Know About TypeScript Union Types

By Cam McHenry on (Updated on )

SummaryUnion types are a powerful feature of TypeScript to ergonomically model a finite number of mutually exclusive cases and ensure that every case is handled safely.


Programming in TypeScript is all about creating models that help us to write safe code. Among those models, union types are one of the most useful, because they allow us to model mutually exclusive states like: low, medium, or high, one or none, on or off, and so on. In this article, I'll teach you what a union type is, when to use it, and tips on how to use it effectively.

Contents

What is a union type in TypeScript?

A union type (or "union" or "disjunction") is a set of types that are mutually exclusive. The type represents all of the possible types simultaneously. A union type is created with the union operator |, by listing out each type and separating them with a pipe character.

type Union = "A" | "B" | "C";

The union type provides more information to the TypeScript compiler that allows it to prove code is safe for all possible situations, which is a powerful tool. We may not know whether the user will pass a string, number, or object (for example) to a function, but we can guarantee that every case is handled without needing to write any unit tests to check that.

When should you use a union type?

Union types are a perfect fit for a situation where we know exactly what all of the possible states are, but we don't know when we compile the program which one will be used. For example, we could use union types to store:

As a counterexample, something like a person's name is not a good fit for a union type, because there are essentially an infinite (or very large) number of possible states.

Examples of union types

In the DOM, we can only store strings for values, or numbers as strings. So, the only acceptable types for a DOM value are essentially a string or a number. (This is exactly the definition of the ReactText type).

// Acceptable DOM values
type Value = string | number;

Similarly, DOM events are always happen independently of each other (events are processed one at a time). So, there is a finite list of possible events that can be processed:

type Event = MouseEvent | KeyboardEvent; /* and so on */

We can also use a union type to represent a subset of primitive types like string or number.

For example, we could write some business logic functions that only accept days of the week:

type DayOfWeek =
  | "Monday"
  | "Tuesday"
  | "Wednesday"
  | "Thursday"
  | "Friday"
  | "Saturday"
  | "Sunday";

function isBusinessDay(day: DayOfWeek): boolean {
  return day !== "Saturday" && day !== "Sunday";
}

isBusinessDay("Monday"); // => true
isBusinessDay("Saturday"); // => false
isBusinessDay("Whensday");
//             ^^^^^^^^ ERROR: Argument of type '"Whensday"'
// is not assignable to parameter of type 'DayOfWeek'

If every type in the union is the same, then we can use functions and operators as expected on those types.

type NumberOfColumns = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12;

function getColumnWidth(totalWidth: number, columns: NumberOfColumns) {
  return `${(totalWidth / columns).toFixed(2)}px`;
}

getColumnWidth(1920, 6); // => "320.00px"
getColumnWidth(1920, 16);
//                   ^^ ERROR: Argument of type '16' is not
// assignable to parameter of type 'NumberOfColumns'

If the types are different (which is most of the time), then we cannot simply call functions on them or use arithmetic operators. We need to differentiate between the types in the union.

How to tell which type is currently in use

Of course, it's great that we can model mutually exclusive states with union types, but how do we actually use them? What if every type is not the same? How do we make sense of a union and figure out which specific case we have?

We can differentiate between types in a union with a type guard. A type guard is a conditional check that allows us to differentiate between types. And in this case, a type guard lets us figure out exactly which type we have within the union.

There are multiple ways to do this, and it largely depends on what types are contained within the union. I cover this topic in much more detail here on my post about type guards.

However, there is a shortcut to making differentiating between types in a union easy.

Enter discriminated unions.

What is a discriminated union?

A discriminated union (also called "distinguished union" or "tagged union") is a special case of a union type that allows us to easily differentiate between the types within it.

This is accomplished by adding a field to each type that has a unique value, which can be used to differentiate between the types using an equality type guard.

For example, if we had a type which represented all possible events that could occur, we could give each event a unique name. Then, we just need to check the event name to know exactly what type/case we are handling.

type AppEvent =
  | { kind: "click"; x: number; y: number }
  | { kind: "keypress"; key: string; code: number }
  | { kind: "focus"; element: HTMLElement };

function handleEvent(event: AppEvent) {
  switch (event.kind) {
    case "click":
      // We know it is a mouse click, so we can access `x` and `y` now
      console.log(`Mouse clicked at (${event.x}, ${event.y})`);
      break;
    case "keypress":
      // We know it is a key press, so we can access `key` and `code` now
      console.log(`Key pressed: (key=${event.key}, code=${event.code})`);
      break;
    case "focus":
      // We know it is a focus event, so we can access `element`
      console.log(`Focused element: ${event.element.tagName}`);
      break;
  }
}

In this example, the advantage is that we can have completely disparate types in our union, and easily handle each case with just a single if check. This lends itself well to extension, because we can easily add new events and new cases to our application and lean on TypeScript to ensure that we don't forget to handle them.

How to get a single type from a union type

Sometimes, we want to deal with just a single type from union type, or a subset of the types. Thankfully, TypeScript provides a built-in utility type called Extract to extract a single type from a union type.

Using the DayOfWeek type from before, we can extract individual days from the type:

type DayOfWeek =
  | "Monday"
  | "Tuesday"
  | "Wednesday"
  | "Thursday"
  | "Friday"
  | "Saturday"
  | "Sunday";

type BusinessDay = Extract<
  DayOfWeek,
  "Monday" | "Tuesday" | "Wednesday" | "Thursday" | "Friday"
>;
// => "Monday" | "Tuesday" | "Wednesday" | "Thursday" | "Friday"
type Weekend = Extract<DayOfWeek, "Saturday" | "Sunday">;
// => "Saturday" | "Sunday"

This might seem redundant, but the advantage is that we are deriving types based on our DayOfWeek type. So, if the base type ever changes, we can be sure that all of our types are still valid.

But, Extract is more powerful than just extracting a single type. It can extract all assignable types from a union type.

// Type for a configuration value that can be defined in multiple ways:
// either as a single value (string or number), array of values, or an object.
type Value = string | number;
type Config = Value | Array<Value> | Record<string, Value>;

// Only config values that are assignable to objects will have this type
type Objects = Extract<Config, object>;
// => Value[] | Record<string, Value>

How to get a subset of a union type

We saw that Extract can be used to a subset of a union type, but only for a few specific types. When we want to extract most types, we can use the complement of Extract type, which is Exclude.

In TypeScript, we can use the Exclude type to get all types from a union type, except for those that are assignable to another union.

For example, let's redefine our types derived from DayOfWeek to use Exclude instead:

type BusinessDay = Exclude<DayOfWeek, "Saturday" | "Sunday">;
// => "Monday" | "Tuesday" | "Wednesday" | "Thursday" | "Friday"
type Weekend = Exclude<
  DayOfWeek,
  "Monday" | "Tuesday" | "Wednesday" | "Thursday" | "Friday"
>;
// => "Saturday" | "Sunday"

These types are exactly the same as the ones we defined before, but we defined them using Exclude instead of Extract.

When to use Extract or Exclude

For the most part, Extract and Exclude are interchangeable, they are just complements of each other. So the general rule for when to use them is:

  • Use Extract when you only need to extract a few types from a union type
  • Use Exclude when you need to extract most types from a union type

Both of these types become even more powerful when we leverage each of their respective strengths. For example, we can redefine our day of the week types to use Extract and Exclude in combination:

type Weekend = Extract<DayOfWeek, "Saturday" | "Sunday">;
// => "Saturday" | "Sunday"

type BusinessDay = Exclude<DayOfWeek, Weekend>;
// => "Monday" | "Tuesday" | "Wednesday" | "Thursday" | "Friday"

This version is both much shorter (so it is easier to read) and it also better conveys the meaning and intention behind the types.

When should you not use a union type?

Although union types are an excellent modeling tool, there are legitimate reasons to not use them:

  • When the types are known at compile-time, we can use generics instead to provide further type safety and flexibility. If the types are known ahead of time, then there is no need to use a union type.
  • When we need to enumerate all possibilities at run-time
  • When we want named values

These are just a few common reasons to not use a union type, but there are others as well. If you are interested in knowing the comprehensive difference union types and other language features, check out my article on the differences between union types, enums, and objects.

Conclusion

Union types are a fantastic feature of TypeScript. They are an ergonomic way to model a finite number of mutually exclusive cases, and allow new cases to be added without breaking any existing code. However, union types do not exist at compile-time, so any programs that need access to the enumerated values should probably use an object instead.

If you're interested in learning more about union types and the theory behind them, check out these additional resources:

If this post helped you understand union types better, consider sending me a message to me (@cammchenry) and let me know what you thought. Happy coding!