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 TypeScript know it can use a specific type, called a type guard. A type guard is a block of code in which we narrow the definition of a union type to the extent 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 our method to take in multiple different types of arguments, so we might have a method like this:

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. We therefore want to have a type guard based on the type. How we create a type guard depends on what kind of types we used to create our union types.

In this case, where the types are all JavaScript primitive types, we 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 can treat log as a string. Inside the second, TypeScript will treat log as a boolean. If necessary, we can also do a negative check that would look like typeof !== symbol.

typeof carries a serious limitation, however. That is, it only acts as a type guard with those four primitive types. Specifically, typeof does not allow us to differentiate between different classes, they are all of the type object.

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

Important here is that we can use attributes specific to each class, such as width or radius, inside each if block, because TypeScript uses instanceof to infer the type.

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

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

Get hands-on with 1400+ tech skills courses.