ยท best practices

Filtering arrays in TypeScript with correct types

This article explains how to filter arrays in TypeScript while maintaining correct types. It demonstrates how to create a type guard to ensure that the filtered array only contains the desired type. It also discusses the downsides of type guards and compares them to assertion functions.

In this tutorial I will show you how you can filter arrays properly in TypeScript, getting back their respective values and types.

Demo Code

Let's say we have a type called ResponseData which contains a data property of type string. Next up, we will create an array of items, including some ResponseData objects. We will create an object with data "Banana" and another one that keeps data "Dog". To create an inhomogenous set, we will also add some undefined values to the mix:

type ResponseData = {
  data: string;
};
 
const items = [{ data: 'Banana' }, undefined, { data: 'Dog' }, undefined];

With the demo values set, we can now add type annotations to our items. We'll inform TypeScript that our array consists of a union of ResponseData values and undefined values:

type ResponseData = {
  data: string;
};
 
const items: (ResponseData | undefined)[] = [{ data: 'Banana' }, undefined, { data: 'Dog' }, undefined];

Filtering items in TypeScript

Next, we need to filter our items to only return the ones that are defined. We'll implement a filter that checks for non-undefined values. The filter removes items for which the condition evaluates to false:

type ResponseData = {
  data: string;
};
 
const items: (ResponseData | undefined)[] = [{ data: 'Banana' }, undefined, { data: 'Dog' }, undefined];
 
const payloads = items.filter((item) => item !== undefined);
 
console.log(payloads);

At first glance, everything seems fine. However, upon closer inspection, our IDE still considers our payloads as potentially untyped. They could be either of type ResponseData or undefined. Ideally, we would like our filter to affect the inferred type.

Filtering items with a type guard

We can achieve this by converting our filter into a type guard. To do so, we simply add a type predicate to our filter function using the syntax item is ResponseData. This small adjustment will ensure that we obtain the desired response type and provide better autocompletion support in our IDE:

type ResponseData = {
  data: string;
};
 
const items: (ResponseData | undefined)[] = [{ data: 'Banana' }, undefined, { data: 'Dog' }, undefined];
 
const payloads = items.filter((item): item is ResponseData => item !== undefined);
 
console.log(payloads);

Making type guards resuable

To make it more explicit, we can extract our type guard into a separate function. Let's create a function called isResponseData that takes an item as an input parameter. The item can be of the union type, which includes ResponseData or undefined.

The return type will be a type predicate indicating that our item is of type ResponseData. Our type guard will evaluate to true if the input item is not undefined. Once we have set up the type guard, we can supply it to the Array's filter function:

type ResponseData = {
  data: string;
};
 
function isResponseData(item: ResponseData | undefined): item is ResponseData {
  return item !== undefined;
}
 
const items: (ResponseData | undefined)[] = [{ data: 'Banana' }, undefined, { data: 'Dog' }, undefined];
 
const payloads = items.filter(isResponseData);
 
console.log(payloads);

Downsides of type guards

I also want to show you the downsides of type guards. It is possible to assume invalid types, for example we could say that the items are undefined in the case that they are not. In such cases, the type predicate will override TypeScript's compiler, leading to feedback that our payloads are undefined during design time:

type ResponseData = {
  data: string;
};
 
function isResponseData(item: ResponseData | undefined): asserts item is undefined {
  return item !== undefined;
}
 
const items: (ResponseData | undefined)[] = [{ data: 'Banana' }, undefined, { data: 'Dog' }, undefined];
 
const payloads = items.filter(isResponseData);
 
console.log(payloads);

Assertion Functions

By using the asserts keyword, we can transform our type guard into an assertion function. This approach requires an adjustment to our type guard implementation. Instead of returning a boolean value, we throw an error if something unexpected occurs. If the input item passes the check, TypeScript's compiler will assume it has the ResponseData type based on our assertion signature:

type ResponseData = {
  data: string;
};
 
function isResponseData(item: ResponseData | undefined): asserts item is ResponseData {
  if (item === undefined) {
    throw new Error('It is undefined');
  }
}
 
const items: (ResponseData | undefined)[] = [{ data: 'Banana' }, undefined, { data: 'Dog' }, undefined];
 
const payloads = items.filter(isResponseData);
 
console.log(payloads);

This approach requires us to perform checks on our types. Instead of returning a boolean value, we throw an error if something unexpected occurs. If the input item passes the check, TypeScript's compiler will assume it has the ResponseData type based on our assertion signature. If the input fails the check, an error will be thrown.

Type Guards vs. Assertion Functions

When deciding between a custom type guard or an assertion function, keep the following in mind: Assertion functions are better suited for validators that need to reject inputs at runtime, while type guards are great for narrowing down a type during design time.

Video Tutorial

Back to Blog