If you've ever used Array.filter to filter a list to certain type of items, you've probably been hit by TypeScript not realizing what type of items your list contains after filtering. To fix this, you can use user-defined type guards.
Let's say we have content items of two type: Post and Image.
interface Post {text: string;title: string;date: Date;}interface Image {url: string;alt: string;date: Date;}
We are going to store items of both types in a array.
type Content = Post | Image;const content: Content[] = [{title: "A post",text: "...",date: new Date()},{alt: "A image",url: "...",date: new Date()}]
Now if we want to get a list of all the post titles, we can do it by first filtering filtering with "title" in obj
. But even though we know this works and there's no way title
is undefined, we still get a type error.
const postTitles = content.filter(c => "title" in c).map(c => c.title);// Property 'title' does not exist on type 'Content'.// Property 'title' does not exist on type 'Image'.(2339)
This is because TypeScript can't deduce that all the remaining items are of the type Post. We can fix this issue with a TypeScript feature called user-defined type guards
What are user defined type guards?
Type guards allow narrowing a type with differenty ways. For example, you can use typeof or instanceof.
User-defined type guards are functions whose return type is a type predicate.
function isPost(content: Content): content is Post {return "title" in content;}
Notice the return value of the function is content is Post
this tells TypeScript the function will only return true if content
is of type Post
. The first part of a type predicate (here content) must be a name of parameter of the function.
Now we can use this type guard in our Array.filter and TypeScript won't yell at us anymore 🥳
const postTitles = content.filter(isPost).map(c => c.title);
If you want to play with the code in this article, checkout this playground