type Square = {
type: 'square';
size: number;
};
type Circle = {
type: 'circle';
radius: number;
};
type Shape = Square | Circle;
function getSquaresA(shapes: Shape[]): Square[] {
// Error: Property 'size' is missing in type 'Circle' but required in type
return shapes.filter(shape => shape.type === 'square');
}
function getSquaresB(shapes: Shape[]): Square[] {
// We could use a type guard, but this still could result in this sort of error.
return shapes.filter((shape): shape is Square => shape.type === 'circle');
}
function getSquaresC(shapes: Shape[]): Square[] {
// We could use casting, but this could result in this sort of error.
return shapes.filter(
(shape): shape is Square => shape.type === 'circle',
) as Square[];
}
// Works, but this is much longer.
function getSquaresD(shapes: Shape[]): Square[] {
const squares: Square[] = [];
for (const shape of shapes) {
if (shape.type === 'square') squares.push(shape);
}
return squares;
}
// Is there a nice short way that's a bit more type safe?
#What's a nice short way to filter types?
33 messages · Page 1 of 1 (latest)
tldr: type guards and casting are undesirable as they're effectively forcing a type.
I could use a for loop, but this is a lot more lines. Is there an easier in-built way?
flatMap is probably the most common solution
flatMap(obj => predicate(obj) ? [obj] : [])
!ts
type Square = { type: 'square'; size: number; };
type Circle = { type: 'circle'; radius: number; };
type Shape = Square | Circle;
function getSquares(shapes: Shape[]) {
// ^? - function getSquares(shapes: Shape[]): Square[]
return shapes.flatMap(shape => shape.type === 'square' ? [shape] : []);
}```
A helper works well for this:
Preview:```ts
type Square = {
type: "square"
size: number
}
type Circle = {
type: "circle"
radius: number
}
type Shape = Square | Circle
const guard =
<T, U extends T>(
fn: (x: T) => [U] | undefined | null | false
) =>
(x: T): x is U =>
!!fn(x)
...```
You can choose specific lines to embed by selecting them before copying the link.
Using the guard helper, it will error if you implement the type guard incorrectly.
Wish something like this was in-built
i was gonna say one of typescript's goals is to not add any runtime code...
... but to be fair, we don't need extra runtime code to have type-safe type predicates
I'm also confused why the original guard (i.e. is Square) doesn't actually do what I think it would do.
i wonder if there's an open issue for this
it feels like the guard is just another type of cast
getSquaresB should work tho
Correct, TS type guard isn't type safe, you are responsible for checking the implementation to be correct.
it shouldn't be called a guard then 😦
i think in most cases, type guards are supposed to be complex pieces of code
plus, i suspect type guards came well before narrowing was as good
also note that narrowing doesn't narrow entire objects (unless they're discriminated unions), so automatically inferred typeguards for object types currently doesn't make too much sense
thank you all. I'll go read up on this more
Yeah it's why I tend to avoid using type guards in my code
It's easy to see (shape): shape is Square => shape.type === 'square' is correct when you first write it, but you may introduce issues during refactor.
Personally my take is:
- For validation, use a library (which basically writes type guards for you and ensures they are correct)
- Otherwise, avoid type guards as much as possible.
- For unavoidable situations like
Array#filter, use either generic type guards or theguardhelper I gave earlier.
I found a nice way to write it
type Square = {
type: 'square';
size: number;
};
type Circle = {
type: 'circle';
radius: number;
};
type Shape = Square | Circle;
const maybeSquare = (shape: Shape): Square | undefined =>
shape.type === 'square' ? shape : undefined;
const getSquares = (shapes: Shape[]): Square[] =>
shapes.flatMap(shape => maybeSquare(shape) ?? []);
"nice" being somewhat subjective
That seems like it's a more verbose version of @patent field's answer, no?