DEV Community

loading...
Cover image for TypeScript Type Guards and Type Predicates

TypeScript Type Guards and Type Predicates

smeijer profile image Stephan Meijer ・3 min read

Union types enable us to accept parameters of multiple, different types. Provide either type x or y. Sometimes, these types aren't 100% compatible. They serve the same goal but have different properties. At a later stage, we might want to run some code based on the exact type. This is where type guards and type predicates come in.

Getting Started

Let's start with declaring some types. I like to look at some code when trying to explain stuff. It makes me grasp the concept better.

Assume that we are building a blog and have two types, that form a single union.

type Article = {
  frontMatter: Record<string, unknown>;
  content: string;
}

type NotFound = { 
  notFound: true;
}

type Page = Article | NotFound;
Enter fullscreen mode Exit fullscreen mode

The concrete types are Article and NotFound, while Page is the union. The goal is to write a function to render a page. I'm not going into details about the requirements of checking if a blog exists, and when to invoke that notFound function, but imagine that we have a single render function. Based on the contents in the database, we render either the article, or a not found page. Something like:

function handleRequest(slug: string): Page {
  const article = db.articles.findOne({ slug });
  const page = article ?? { notFound: true };
  return render(page);
}
Enter fullscreen mode Exit fullscreen mode

The challenge that we're dealing with, is when we need to know if handleRequest returned an Article or a NotFound type. In JavaScript, you'd use something like:

function render(page: Page) {
  if (page.content) {
    return page.content;
  }

  return '404 β€” not found';
}
Enter fullscreen mode Exit fullscreen mode

But in TypeScript, that's not going to work. It will throw an Error mentioning that the property content does not exist on type Page.

Property 'content' does not exist on type 'Page'.
  Property 'content' does not exist on type 'NotFound'.
Enter fullscreen mode Exit fullscreen mode

That's because not all the types in the union include that property. To fix this, we need to add a type guard.

Type Guard

A type guard is an expression that performs a runtime check that guarantees the type in the current scope.

The quick fix is to replace that page.content check with something TypeScript would understand:

function render(page: Page) {
  if ('content' in page) {
    return page.content;
  }

  return '404 β€” not found';
}
Enter fullscreen mode Exit fullscreen mode

This works, but it does come at a maintainability cost. The benefit of TypeScript is that it will warn us when we remove a property that's being used. With this change, TypeScript won't warn us when we rename the content property to body for example. Or when we made a typo in 'content'.

This is why type predicates are interesting.

Type Predicate

The type predicate, is the return type of a function like this:

function isArticle(page: Page): page is Article {
  return 'content' in page;
}
Enter fullscreen mode Exit fullscreen mode

It's not the whole function that's the predicate. The predicate is page is Article. Also good to know, 'content' in page is not a type guard in this context. It's a simple expression. The type guard is the if statement that causes TypeScript to narrow the type.

So, the function above looks quite similar to that earlier type guard and comes with the same maintainability issue. But, now that we've extracted it, we can also solve that.

function isArticle(page: Page): page is Article {
  return typeof (page as Article).content !== 'undefined';
}
Enter fullscreen mode Exit fullscreen mode

This works and will error when we refactor Article and remove the content property.

Functions that are declared as type predicate, must return a boolean. When the return value is true, TypeScript assumes that the return type is the one that's declared in the type predicate. If this function returns true, TypeScript assumes that the provided argument page is of type Article.

When we'd call this method inside our render function:

function render(page: Page) {
  if (isArticle(page)) {
    return page.content;
  }

  return '404 β€” not found';
}
Enter fullscreen mode Exit fullscreen mode

TypeScript knows that page.content exists, because inside the if scope, page is of type Article. The if (isArticle(page)) expression, is a type guard.

After the if statement, page is not of type Article. And because our union only has 2 types, TypeScript is also aware that it must be of type NotFound at that stage.


πŸ‘‹ I'm Stephan, and I'm building rake.red. If you wish to read more of mine, follow me on Twitter.

Discussion (4)

pic
Editor guide
Collapse
andi1984 profile image
Andreas Sander

Nice article, @stephanmeijer !

Functions that are declared as type predicate, must return a boolean. When the return value is true, TypeScript assumes that the return type is the one that's declared in the type predicate. If this function returns true, TypeScript assumes that the provided argument page is of type Article.

I learned about type predicates in the "TypeScript in 50 lessons" book and I was blown away by that, so I tried to explain myself in a video. But I really think your way of saying it is much more comprehensive :).

Collapse
2n2b1 profile image
kyle

I can’t speak to the correctness of this implementation, however, I enjoy it’s simplicity! Very well done. πŸ‘πŸ»

Collapse
kirkcodes profile image
Kirk Shillingford

I've been looking for a good explanation as to when to use type predicates. This was excellent!

Collapse
smeijer profile image
Stephan Meijer Author

Thanks Kirk! Appreciated.