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.