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:
- Create a mapped array of
.shapes
. For each item we will perform type narrowing and then return the narrowed type orundefined
- Filter out all
undefined
records
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.