Union Types and Type Guards

Let's look at union types and type guards in this lesson.

We'll cover the following

Type guards

TypeScript calls the functionality that lets us use a specific type a type guard. A type guard is a block of code where you narrow the definition of a union type in such a way that TypeScript can infer the narrower type and use that information to type check the code.

For example, one use of a union type is to allow your method to take in multiple different types of arguments, so you might have a method like this, admittedly a little contrived:

const logAThing(log: number | string | boolean | symbol) {
}

We want to treat the log argument differently based on its type, which likely involves calling methods that only make sense for the actual type of the argument, not the union type. This means that we want to be able to have a type guard based on the type. The way in which we can create a type guard depends on what kind of types we have making up our union types.

In this case, where the types are all JavaScript primitive types, you can use the keyword typeof as a type guard. Using typeof only works on the four types shown in this snippet:

const logAThing(log: number | string | boolean | symbol) {
  if (typeof log === string) {
    logString(log)
  } else if (typeof log === boolean) {
    logBoolean(log)
  }
  // ...and so on
}

Inside the first if statement, TypeScript is able to treat log as a string, and inside the second, TypeScript will treat log as a boolean. You can also do a negative check, which would look like typeof !== symbol, if there’s some reason why that makes sense to do so.

Using typeof has a serious limitation, however. It only acts as a type guard with those four primitive types. Specifically, typeof does not allow you to differentiate between different classes—they are all of object type.

instanceof type guards

For differentiating between classes, TypeScript provides instanceof type guards. An instanceof type guard behaves the same way as typeof and works on any type that is created with a constructor. So we can do something like this:

const area(thing: Square | Triangle | Circle): number {
  if (thing instanceof Square) {
    return thing.width * thing.height
  } else if (thing instanceof Triangle) {
    return thing.width * thing.height * 0.5
  } else if (thing instanceof Circle) {
	  return PI * 2 * thing.radius
  }
}

The important bit here is that inside each if block, we can use attributes specific to each class, like width or radius, because TypeScript uses instanceof to infer the type.

Another important extension here is that we don’t need to specify that the else branch is a Circle, TypeScript will infer that the else branch implies all the types in the union type not covered by the if branch.

This doesn’t exactly solve our reducer issue, because our reducer actions aren’t created with JavaScript constructors, they are just JavaScript literals. TypeScript provides a different way to type guard generically on the existence of a particular field in the type.

Note that we can also have a union of primitive and object types and then use typeof and instanceof in the if-else.

We can use the in keyword to test for the existence of a particular attribute in the object, as shown here:

const area(thing: Square | Triangle | Circle): number {
  if ("radius" in thing) {
    return PI * 2 * thing.radius
  } else {
	  return thing.width * thing.height
  }
}

What’s happening here is subtle, and this code actually has a bug compared to the previous code. The in operator returns true if the object on the right contains the attribute on the left. From a typing perspective, this also acts as a type guard, and inside the if block protected by the in statement, TypeScript works on a list of available attributes based on the elements of the union type that contain that attribute.

If multiple component types contain the attribute—both Square and Triangle have a width attribute, for example—then inside the type guard, TypeScript will only allow you to access an attribute that is available in all the component types with that attribute. In our case, we now have a bug since both Square and Triangle would go down the second branch, but the formula is for square, which should give you a sense of the limitations of this mechanism.

If you think the in operator is too hard to understand, you can write your own functions and have TypeScript treat them as type guards. The syntax for this is frankly a little verbose and weird:

function isCircle(shape: Square | Triangle | Circle): shape is Circle {
  return (shape as Square).radius !== undefined
}

Okay, lots of pieces here. The function takes an argument. You would typically have the entire union type as the type of the argument (otherwise, you wouldn’t be able to call the function with any potential member of the union type). The return value of the function is what TypeScript calls a type predicate and the syntax is <variable> is <type>, the variable name has to match the name of the argument to the function.

The body of the function is whatever you want, basically, as long as the function returns true when you can show that the argument is of the type needed. The recommended way to do that is to check for the existence of an attribute that is in that class. In our case, we’re checking to see if the shape is a circle by testing for the existence of a radius attribute.

We can then use this function as we’ve been using our other type guard:

const area(thing: Square | Triangle | Circle): number {
  if (isCircle(thing)) {
    return PI * 2 * thing.radius
  } else {
	  return thing.width * thing.height
  }
}

This code is bug-for-bug compatible with the previous snippet, in that all objects deemed a Circle will go down the if branch, and all not-Circle objects will go down the else branch.

There are some clear limitations in doing type guards based on whether a single attribute is present or absent. As in our example, there might be multiple types with the same attribute and therefore, the type check might not be exact enough.

TypeScript, however, is extremely flexible in how it allows you to signal types, and while the type guards we’ve looked at in this section can be great, especially when dealing with external code, we do have other options.

Get hands-on with 1200+ tech skills courses.