#What's a nice short way to filter types?

33 messages · Page 1 of 1 (latest)

wheat lava
#
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?
#

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?

patent field
#

flatMap is probably the most common solution

#

flatMap(obj => predicate(obj) ? [obj] : [])

wanton jewelBOT
patent field
#

!ts

wanton jewelBOT
#
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] : []);
}```
cedar stratus
#

A helper works well for this:

wanton jewelBOT
#
nonspicyburrito#0

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)
...```

cedar stratus
#

Using the guard helper, it will error if you implement the type guard incorrectly.

wheat lava
#

Wish something like this was in-built

patent field
#

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

wheat lava
#

I'm also confused why the original guard (i.e. is Square) doesn't actually do what I think it would do.

patent field
#

i wonder if there's an open issue for this

wheat lava
#

it feels like the guard is just another type of cast

patent field
#

getSquaresB should work tho

cedar stratus
#

Correct, TS type guard isn't type safe, you are responsible for checking the implementation to be correct.

wheat lava
#

it shouldn't be called a guard then 😦

patent field
#

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

wheat lava
#

thank you all. I'll go read up on this more

cedar stratus
#

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 the guard helper I gave earlier.
wheat lava
#

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

cedar stratus
#

That seems like it's a more verbose version of @patent field's answer, no?

wheat lava
#

oh no way, i didn't even see that solution

#

My bad

#

well, it's funny that I ended up in the same place lol