← Posts

Filtering in TypeScript

Lets assume we have the following shapes array setup:

type Circle = { radius: number };
type Square = { size: number };
type Shape = Circle | Square;

const shapes: Shape[] = [{ radius: 10 }, { size: 20 }, { radius: 15 }];

Now we want to filter circles:

const circles = shapes.filter((shape) => "radius" in shape);

However TypeScript still doesn't actually know we're left with circles, if we attempt to do the following:

const radius = circles[0].radius; // Property 'radius' does not exist on type 'Shape'.

So we can use type predicates to give TypeScript a hint:

const isCircle = (shape: Shape): shape is Circle => 'radius' in shape;
const circles = shapes.filter(isCircle);
Now the following works:
const radius = circles[0].radius; // const radius: number

But what happens if our type predicate is wrong?

// TypeScript doesn't actually validate this is a circle, it acts more like a cast
const isCircle = (shape: Shape): shape is Circle =>
  "some-other-property" in shape;
const circles = shapes.filter(isCircle);
// This actually isn't true at all, `radius` might not exist
const radius = circles[0].radius; // const radius: number

So this isn't great. There is another option though which will allow TypeScript to correctly determine the narrowed type without having to use a type predicate. To make it work we'll:

And we're left with an array of narrowed types, no type predicates needed!

Warning: If you're dealing with filtering very large datasets you're probably better off sticking with type predicates.

const typesafeFilter = <T, TReturn>(
  items: T[],
  filter: (item: T) => TReturn | undefined
): TReturn[] =>
  items.map(filter).filter((item): item is TReturn => Boolean(item));

const circles = typesafeFilter(shapes, (shape) =>
  "radius" in shape ? shape : undefined
);

// TypeScript figured out this was a circle!
const radius = circles[0].radius; // const radius: number;

Better yet if we make a typo, TypeScript will complain:

const circles = typesafeFilter(shapes, (shape) =>
  "radius-typo" in shape ? shape : undefined
);
const radius = circles[0].radius; // Property 'radius' does not exist on type '(Circle & Record<"radius-typo", unknown>)

Then go about with your other business.