Union types

Like intersection types, union types allow us to combine the properties of existing primitive and custom types to make composites.

Intersection types work well when you have complete knowledge of what form your data will take. This approach only works if you know in advance that your data will be and that it will share properties from more than one type. But what if you don’t have this knowledge? You only know that it will be one of the three possible types? This is where union types become helpful. Instead of saying that event X will be of type A we say that event X may be one of types A or B or C.

A union type is a type formed from two or more other types, representing values that may be any one of those types. We refer to each of these types as the union’s members.

Demonstration

type Petrol = {
  maxSpeed: number;
  tankVolume: number; // unique property
};

type Electric = {
  maxSpeed: number;
  maxCharge: number; // unique property
};

type Car = Petrol | Electric;

const tesla: Car = {
  maxSpeed: 120,
  maxCharge: 22,
};
  • We have defined two custom types: Petrol and Electric
  • We have unified them to make a new custom type of Car
  • tesla is an instance of the union type

Usage with functions

Union type syntax is not limited to the declaration phase. You can also use them as parameters to functions or function return types:

function generateCar(car: Petrol | Electric): void {
  console.log(`The car has a max speed of ${car.maxSpeed}`);
}

Unifying primitive and custom types

We can also use unions with primitive types as well as custom types:

function printId(id: number | string): void {
  console.log(`Your id is ${id}`);
}

We can also combine primitive and custom types in the same union:

function printStringOrNumber(): string | Petrol {}

Set theory: understanding errors

In JavaScript we have the primitive types of string , number , boolean etc. In TS, these types are distinct and exclusive: if you declare that x : string you cannot redefine it as boolean or expect Boolean properties and methods to belong to a string type. For instance a string type is incapable of having as its value false.

This is not true in the case of unions.

With a union of string | boolean a value x could be 'banana' or true. We are allowing for all values that are either from the set string or the set boolean. Logically this is equivalent to the OR operator: a value of type string | boolean could be one of infinite possible strings or true/false. With union types then, we radically widen the scope of possible values for the type. a

These constraints don’t just apply to the values themselves but the methods and properties you can use with the values. For instance a boolean doesn’t have the property of length. Therefore if you have a union type of string|boolean you are going to get an error if you apply length to it since the property must exist on both types that comprise the union. The way round this is to use type narrowing and execute control flow based on the parameter that is passed to a function that is declared to be of this mixed type.

Here is an example of this:

function printId(id: number | string) {
  if (typeof id === "string") {
    // In this branch, id is of type 'string'
    console.log(id.toUpperCase());
  } else {
    // Here, id is of type 'number'
    console.log(id);
  }
}