How To Do Anything in TypeScript With Type Guards

camchenry

Cameron McHenry

Posted on November 3, 2021

How To Do Anything in TypeScript With Type Guards

This article was originally published on my blog: camchenry.com. If you enjoy this article, please consider joining my mailing list for more content like this one.


TypeScript is valuable because it enables us to write safe code. Because when every type in the code is known at compile time, we can compile the code with TypeScript and perform type checking, which ensures that the code will not crash or cause errors.

However, it is not always possible to know every type at compile time, such as when accepting arbitrary data from an external API. To check types at run-time or differentiate between different types, we to need narrow the types using a type guard.

What is narrowing?

In TypeScript, narrowing is the process of refining broad types into more narrow types. Narrowing is useful because it allows code to be liberal in the types that it accepts. Then, we can use type guards to narrow the type down to something more useful.

These are some common examples of narrowing:

  • unknown or any to string
  • string | object | number to string
  • number | null | undefined to number
  • string to a custom type like NonEmptyString

What is a type guard?

A type guard is a kind of conditional check that narrows a type. Type guards allow for run-time type checking by using expressions to see if a value is of a certain type or not.

So, what does a type guard look like? These are all examples of type guards:

  • typeof value === 'string'
  • 'name' in data
  • value instanceof MouseEvent
  • !value

A type guard is a special kind of expression that changes the type of a variable. We will look at more examples of type guards in practice later.

The kinds of type guards (how to check a type)

Most type guards revolve around regular JavaScript operators, which are given extra abilities in TypeScript that make it possible to narrow types by writing typical JavaScript code. So, it is possible that you've used a type guard before without even realizing it!

Fundamentally, every type guard relies on checking that some expression evaluates to true or false.

As a result, the first kind of type guard which we will look at is a simple truthiness check. But we can utilize more complex type guards like in, typeof, and instanceof that tell us much more information.

In addition to all of these built-in type guards, we can can go even further and create our own custom type guards that can check any type.

Boolean type guard (truthiness)

As stated previously, checking the truthiness of a value is the essence of all type guards.

However, a boolean type guard only checks the truthiness of a value, but gives us no additional information beyond that. Other more complex type guards can check more complex types or verify more properties, but the boolean type guard is the most basic type guard.

function getAvailableRooms(rooms: number | undefined) {
  if (rooms) {
    return `There are ${rooms} hotel rooms available to book.`;
  }
  return "Sorry, all rooms are currently booked.";
}

getAvailableRooms(undefined); // "Sorry, all rooms are currently booked."
getAvailableRooms(5); // "There are 5 hotel rooms available to book."
Enter fullscreen mode Exit fullscreen mode

When using a boolean type guard, the value is implicitly casted to a boolean. This has a logical interpretation most of the time, but not always.

For example, if use a boolean type guard to check a type of number | undefined, we might expect that it will only exclude the undefined case. However, it will also rule out the case where the value is 0, which might not be what you expect in some cases. For more information on this common bug, check out Kent C. Dodd's article, "Use ternaries rather than && in JSX."

Equality type guard

In the boolean type guard, we checked the truthiness of an expression. In an equality type guard, we check the value of an expression.

This kind of type guard is useful when we know all of the possible values of a type. For example, if we have an enumeration of string or number values, or if we want to know that a value is not null or undefined.

Here is an example where we use an equality type guard to remove undefined from the type of a variable:

function getGreeting(timeOfDay?: "morning" | "afternoon") {
  if (timeOfDay === undefined) {
    return `Hello!`;
  }
  // Now the type of `timeOfDay` is narrowed to `morning` | `afternoon`,
  // so we can use string methods on it safely.
  return `Good ${timeOfDay[0].toUpperCase()}${timeOfDay.slice(1)}!`;
}

getGreeting(); // "Hello!"
getGreeting("afternoon"); // "Good Afternoon!"
getGreeting("morning"); // "Good Morning!"
Enter fullscreen mode Exit fullscreen mode

We can also use a switch block to accomplish exactly the same thing:

function getGreeting(timeOfDay?: "morning" | "afternoon") {
  switch (timeOfDay) {
    case "afternoon":
    case "morning":
      return `Good ${timeOfDay[0].toUpperCase()}${timeOfDay.slice(1)}!`;
    default:
      return `Hello!`;
  }
}
Enter fullscreen mode Exit fullscreen mode

Using a switch block like this might be preferable if you have a lot of possible values to check and which might share the same code.

Discriminated unions deserve their own article, but they are essentially a more powerful version of the equality type guard.

A discriminated union is a type that has multiple possible types, with a field that allows to us to discriminate (or differentiate) between them. In other words, when we check the value of a single field (like type), it automatically includes a number of other properties.

typeof type guard

In contrast to the previous example, where we checked the value of a variable (or expression), with a typeof type guard, we check the type of a variable.

When there is a value which has several possible types, like string | number, we can use typeof to figure out which type it is.

For example, we can use typeof to write a comparison function that compares two values to each other and returns the difference:

function compare(a: number | string, b: number | string): number {
  if (typeof a === "number" && typeof b === "number") {
    // Both a and b are numbers, so we can compare them directly.
    return a - b;
  }
  if (typeof a === "string" && typeof b === "string") {
    // We can use string methods on `a` and `b` safely.
    return a.localeCompare(b);
  }
  throw new Error(
    `Cannot compare unrelated types '${typeof a}' and '${typeof b}'`
  );
}

compare("a", "b"); // => -1
compare("b", "a"); // => 1
compare(123, 321); // => -198
Enter fullscreen mode Exit fullscreen mode

The biggest limitation of the typeof guard is that it can only differentiate between types that JavaScript recognizes. The types that typeof can check are:

  • boolean
  • string
  • number
  • bigint
  • object
  • symbol
  • function
  • undefined

instanceof type guard

When we have a variable that is an instance of a class, we can use instanceof to check whether if the variable has that type or not.

For example, the DOM APIs define many classes and subclasses which can be quickly checked using instanceof:

function handleEvent(event: Event) {
  if (event instanceof MouseEvent) {
    // `event` now has type `MouseEvent`, so we can access mouse-specific properties
    console.log(`A mouse event occurred at (${event.x}, ${event.y}`);
  }
  if (event instanceof KeyboardEvent) {
    // `event` now has type `KeyboardEvent`, so we can access key-specific properties
    console.log(`A keyboard event occurred: ${event.key} ${event.}`);
  }
  console.log("An event occurred: ", event.type);
}
Enter fullscreen mode Exit fullscreen mode

This is useful when dealing with potentially generic DOM objects, because a single instanceof check grants access to all of the properties and methods of the class.

This can also be used to differentiate between common objects in JavaScript, like Map, Date, Array, or Set. For example, we can create a function to create a lookup table which accepts many possible inputs:

// Creates a Map which returns some value given a string key
// (ignoring the fact that the Map constructor already accepts some of these)
function createLookupTable<Value>(
  db: [string, Value][] | Map<string, Value> | Record<string, Value>
): Map<string, Value> {
  // `db` has type `[string, Value][] | Map<string, Value> | Record<string, Value>`
  if (db instanceof Array) {
    // `db` now has type `[string, Value][]`
    return new Map(db);
  }
  // `db` has type `Map<string, Value> | Record<string, Value>`
  if (db instanceof Map) {
    // `db` now has type `Map<string, Value>`
    return db;
  }
  // `db` has type `Record<string, Value>`
  return new Map(Object.entries(db));
}

createLookupTable([
  ["hat", 14.99],
  ["shirt", 24.95],
]);
// => Map (2) {"hat" => 14.99, "shirt" => 24.95}

createLookupTable(
  new Map([
    ["hat", 14.99],
    ["shirt", 24.95],
  ])
);
// => Map (2) {"hat" => 14.99, "shirt" => 24.95}

createLookupTable({ hat: 14.99, shirt: 24.95 });
// => Map (2) {"hat" => 14.99, "shirt" => 24.95}
Enter fullscreen mode Exit fullscreen mode

Here is another example using instanceof to check if a type is a Date or a string and decide whether to construct a new Date object or not:

function getDate(value: string | Date): Date {
  if (value instanceof Date) {
    return value;
  }
  return new Date(value);
}

getDate("2021-05-06 03:25:00");
// => Date: "2021-05-06T07:25:00.000Z"
getDate(new Date("2021-05-06 03:25:00"));
// => Date: "2021-05-06T07:25:00.000Z"
Enter fullscreen mode Exit fullscreen mode

in type guard

The in type guard allows us to differentiate between multiple types by checking if an object has a specific property. In JavaScript, the in operator, like all type guards, returns a boolean value that indicates if the object has the property or not. For example,

"data" in { name: "test", data: { color: "blue" } }; // => true
"data" in { name: "test", data: undefined }; // => true
"data" in { name: "test" }; // => false
Enter fullscreen mode Exit fullscreen mode

In this way, we can use in to differentiate objects that have different sets of properties. For example, we can use it to differentiate between different types of classes (in this case, events):

function handleEvent(event: MouseEvent | KeyboardEvent) {
  if ("key" in event) {
    // event now has type `KeyboardEvent`
    console.log(`A keyboard event occurred: ${event.key}`);
  } else {
    // event now has type `MouseEvent`
    console.log(`A mouse event occurred: ${event.button}`);
  }
}
Enter fullscreen mode Exit fullscreen mode

The important thing here is that key is only defined for KeyboardEvent, but not for MouseEvent. If the property we check exists in multiple cases, the narrowing will not work. For example, the following code will not work:

type EventInput =
  | { type: "mouse"; button: string }
  | { type: "key"; key: string };

function handleEventInput(event: EventInput) {
  // This type guard will NOT work:
  if ("type" in event) {
    // event still has type `EventInput`, so the type guard does not
    // do any narrowing in this case
  }
}
Enter fullscreen mode Exit fullscreen mode

Though not always related to its use for narrowing types, the in operator is also often used to check for browser support of certain features.

For example, the guard 'serviceWorker' in navigator checks whether the browser supports service workers.

Assertion type guard (or assertion function)

In TypeScript 3.7, TypeScript added support for assertion functions. An assertion function is a function that assumes a condition is always true, and throws an error when it does not.

To create an assertion function, we need to add something called an "assertion signature," which is a formal declaration of what the function will assert. The assertion signature is additional information about a function (like a return type) that lets the TypeScript compiler narrow the type.

Let's look at an example:

function assertString(value: unknown): asserts value is string {
  if (typeof value !== "string") {
    throw new TypeError(`Expected 'string', got: '${typeof value}'`);
  }
}

const x = "123";
assertString(x);
// x now has type 'string', so it is safe to use string methods
x.toLowerCase();
Enter fullscreen mode Exit fullscreen mode

Previously, we discussed how all type guards are based around a boolean check. That is still true in this case, but the actual usage is slightly different from other type guards.

With other type guards, we typically used something like if or switch to create different branches of execution. With an assertion function, the two branches are: continue as normal, or stop the script (throw an error).

Other than the difference of how an assertion type guard can throw an exception, assertion type guards are similar to other type guards. However, something that we must be careful about is accidentally creating a type guard which asserts the wrong condition.

This is one way that we can end up with a false sense of safety. Here is an example where the function asserts something, but the actual code asserts nothing.

function assertString(value: unknown): asserts value is string {
  // This check does not match the assertion signature
  if (typeof value === "boolean") {
    throw new TypeError();
  }
}

const x: unknown = 123;
assertString(x);
// We get a run-time exception here (!!!), which TypeScript should
// be able to prevent under normal circumstances:
x.toLowerCase();
// "TypeError: x.toLowerCase is not a function"
Enter fullscreen mode Exit fullscreen mode

User-defined (custom) type guard

Most type guards have limitations to what they can check, such as only primitive types for typeof, or only classes for instanceof. But with user-defined type guards, there are no limitations on what we can check.

Custom type guards are the most powerful kind of type guard, because we can verify any type, including ones that we defined ourselves, as well as built-in types from JavaScript or the DOM. The main downside of custom type guards is that they are not predefined, so we have to write them ourselves.

There are a few built-in custom type guards though, such as Array.isArray:

const data: unknown = ["a", "b", 123, false];
if (Array.isArray(data)) {
  // data now has type "array", so it is safe to use array methods
  data.sort();
}
Enter fullscreen mode Exit fullscreen mode

In the next section, we will look at all of the different ways that we can define our own type guard functions.

Type guard functions

A type guard function is a function that returns a value and has a type predicate.

A type predicate is an additional declaration that is added to a function (like a return type) which gives additional information to TypeScript and allows it to narrow the type of a variable. For example, in the definition of Array.isArray,

function isArray(arg: any): arg is any[];
Enter fullscreen mode Exit fullscreen mode

the type predicate is arg is any[]. In spoken word, the signature of this function might be: "isArray takes one argument of type any and checks if it is an array." In general, type predicates take the form: variable is type.

For a function to be eligible as a type guard, it must:

  • Return a boolean value
  • Have a type predicate

The type predicate replaces the return type, because a function with a type predicate must always return a boolean value.

Examples of type guard functions

Check if a value is a string

This example is essentially a reusable form of the built-in typeof type guard.

function isString(value: unknown): value is string {
  return typeof value === "string";
}
Enter fullscreen mode Exit fullscreen mode

Check if a value is defined (not null or undefined)

A common use case for type guards is to refine the type of something like Type | null or Type | undefined down to just Type, effectively eliminating the null or undefined case. We can do this by accepting a generic type which can be null or undefined, and adding a type predicate to remove null | undefined from the type.

function isDefined<Value>(value: Value | undefined | null): value is Value {
  return value !== null && value !== undefined;
}
Enter fullscreen mode Exit fullscreen mode

Then, it can be used like this:

const x: string | undefined = 123;
if (isDefined(x)) {
  // x is defined, so it is safe to use methods on x
  x.toLowerCase();
}
Enter fullscreen mode Exit fullscreen mode

Remove all values null or undefined values from array

Using the isDefined type guard we just defined, we can use it with the built-in Array.filter function, which has special support for type predicates. The Array.filter function is defined like:

function filter<Filtered extends Item>(
  predicate: (value: Item, index: number, array: Item[]) => value is Filtered
): Filtered[];
Enter fullscreen mode Exit fullscreen mode

(The definition here has been altered slightly for improved understanding and readability). Essentially, every usage of Array.filter is a type guard, except in most cases the type before and after calling Array.filter is the same type.

But if the function passed to Array.filter narrows the type (like a type guard), then the return type of Array.filter changes. So we can use our isDefined type guard to remove all null and undefined values from the array, as well as removing null and undefined types from the array items.

// 'values' is an array of strings, but can have null or undefined values
const values: (string | null | undefined)[] = [null, "a", "b", undefined];

// We can safely assign 'filtered' to an array of strings (string[])
// because `isDefined` changes the type of the variable 'values'
const filtered: string[] = values.filter(isDefined);
Enter fullscreen mode Exit fullscreen mode

Check if a number is positive

A common use-case for creating our own types is so that we can ensure certain conditions are met. For example, we might want to ensure that an object has certain properties, a string is not empty, or a number is positive.

First, we need to create a custom PositiveNumber type, and a type guard to check for it.

type PositiveNumber = number & { __type: "PositiveNumber" };

function isPositive(n: number): n is PositiveNumber {
  return n >= 0;
}
Enter fullscreen mode Exit fullscreen mode

To create a new type of number, we use a technique called "type branding." Essentially, we add a phantom property to the number type to differentiate it from all other types of numbers. In this case, I chose to use { __type: 'PositiveNumber' }, but we could picked any arbitrary key/value, as long as it is unique and not already defined.

The important thing is that we cannot create PositiveNumber by declaring a variable:

const x: PositiveNumber = 49;
// ERROR: Type 'number' is not assignable to type 'PositiveNumber
Enter fullscreen mode Exit fullscreen mode

This may seem inconvenient, but it is exactly why it allows us to write safe code, because we must always check conditions with the type guard and prevents us from writing code like this:

const x: PositiveNumber = -100;
Enter fullscreen mode Exit fullscreen mode

As an example of how we might use this type guard, we can write a square root function which accepts only positive numbers:

function squareRoot(n: PositiveNumber): PositiveNumber {
  return Math.sqrt(n) as PositiveNumber;
}
Enter fullscreen mode Exit fullscreen mode

Then, we can use the type guard to compute the square root:

const x = 49;

squareRoot(x);
// ERROR: ^^^ 'number' is not assignable to parameter of type 'PositiveNumber'

if (isPositive(x)) {
  // OK: Now x has type 'PositiveNumber', so we can take the square root
  squareRoot(x);
}
Enter fullscreen mode Exit fullscreen mode

Check if a string is a GUID

Similar to the previous example, we can create a custom Guid type that is based on the string type and write a type guard to check for it.

type Guid = string & { __type: "Guid" };

const guidPattern =
  /^[0-9a-f]{8}-[0-9a-f]{4}-[0-5][0-9a-f]{3}-[089ab][0-9a-f]{3}-[0-9a-f]{12}$/i;

function isGuid(value: string): value is Guid {
  return guidPattern.test(value);
}
Enter fullscreen mode Exit fullscreen mode

As an example of how to use this type and type guard in practice, we will create a list of users that can be searched by GUID.

type User = {
  id: Guid;
  name: string;
};
const users: User[] = [
  /* ... */
];

function getUserById(id: Guid) {
  return users.find((user) => user.id === id);
}

const id = "abc123";

getUserById(id);
// ERROR:   ^^ Argument of type 'string' is not assignable to parameter of type 'Guid'

if (isGuid(id)) {
  // OK: id now has type `Guid`:
  getUserById(id);
}
Enter fullscreen mode Exit fullscreen mode

Check if a value is a valid React element (React.isValidElement)

The isValidElement function included with React checks if a value is a valid React element, which can be rendered by React.

function isValidElement<P>(
  object: {} | null | undefined
): object is ReactElement<P>;
Enter fullscreen mode Exit fullscreen mode

The implementation of this function is not relevant here, but it is a perfect example of a common type guard function that verifies a custom type that cannot be verified with other type guards.

Pros and cons of custom type guard functions

Custom type guard functions are powerful and sometimes be the only option in order to write type-safe code. However, they can be a tricky to write and are susceptible to mistakes.

The advantages of custom type guard functions are:

  • Flexibility: can check any type, including custom types that we define
  • Run-time type checking: allows type-checking at run-time, ensuring that safety is ensured both when code is compiled, and also when it is running
  • Reusable: type guard functions allow us to combine multiple type guards into one and easily use them in multiple places

The disadvantages of a custom type guard function are:

  • Manual: type guard functions have to be written manually (currently no automatic way to generate type guards)
  • Performance: using type guard functions has a slight overhead to call the function and run the checks (negligible in practice)
  • Fragile: custom type guards can be implemented incorrectly on accident, which may provide a false sense of security and safety

Where can a type guard be used?

Now that we know all about the available type guards, we will briefly look at where we can use type guards. There are a limited number of places that type guards can be used. The most common place they are used is in a if/else block, like this:

if (typeof value === "string") {
  // value has type 'string' in this block
} else {
  // value does NOT have type 'string' in this block
}
Enter fullscreen mode Exit fullscreen mode

Since we can use type guards in an if/else block, then you might expect that we can also use them with the ternary operator, since it's a shorthand for an if/else block. And you would be correct!

typeof value === 'string'
  ? /* value has type 'string' in this block */
  : /* value does NOT have type 'string' in this block */
Enter fullscreen mode Exit fullscreen mode

In addition, since TypeScript 4.4, we can use type guards with aliased conditions.

const isString = typeof value === "string";
if (isString) {
  // value has type 'string' in this block
} else {
  // value does NOT have type 'string' in this block
}
Enter fullscreen mode Exit fullscreen mode

Beyond just if/else, type guards can also be used in a while block:

while (typeof value === "string") {
  // value has type 'string' in this block
}
Enter fullscreen mode Exit fullscreen mode

Finally, type guards are also compatible with a switch/case block:

switch (typeof value) {
  case "string":
    // value has type 'string' in this block
    break;
}
Enter fullscreen mode Exit fullscreen mode

Conclusion

Type guards are conditional checks that allow types to be refined from one type to another, allowing us to write code that is type-safe and easy to write at the same time. Since TypeScript is a superset of JavaScript, many common operators like typeof or instanceof act as type guards. But, we can also use custom type guards to verify any condition and any type, given enough effort.

Summary

In general, I would recommend using the type guard that feels the most natural, which will come from experience. Don't write a custom type guard function when a simple typeof check can suffice. However, it may be necessary to write a custom type guard.

To summarize the strengths of each type guard, here is a summary table.

Type guard Usage
Boolean / truthiness Rule out falsy values like null, undefined, '', 0, etc.
Equality Narrow multiple possible types down to a single type
typeof Narrow a type to a primitive type (like string or number)
instanceof Check if a value is an instance of a specific class
in Check if a property can be accessed
Assertion function Assert invariants that should always be true
Custom type guard function Check that a type meets some arbitrary conditions

If this article was helpful, let me know on Twitter at @cammchenry! If you enjoy guides like this, consider signing up for my mailing list to be notified when new posts are published.

Good luck, and happy coding!

💖 💪 🙅 🚩
camchenry
Cameron McHenry

Posted on November 3, 2021

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related