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:
.shapes
. For each item we will perform type narrowing and then return the narrowed type or undefined
undefined
recordsAnd 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.