TypeGuards for optional property while filtering array of objects

Issue

I’ve got the following interfaces in typescript:

interface ComplexRating {
  ratingAttribute1?: number;
  ratingAttribute2?: number;
  ratingAttribute3?: number;
  ratingAttribute4?: number;
}

export interface Review {
  rating: ComplexRating | number;
}

I’d love to calculate an average rating for say ratingAttribute1 for sake of simplicity.

So given these reviews:

const reviews: Review[] = [
  { rating: { ratingAttribute1: 5 } },
  { rating: { ratingAttribute1: 10 } },
  { rating: { ratingAttribute2: 15 } },
  { rating: 5 }
]

I can filter down the reviews to the ones I’m interested in i.e.:

const calculateAverageRating = (reviews: Review[]): number => {
  const reviewsWithRating = reviews.filter(
    (review) =>
      typeof review.rating === 'object' &&
      typeof review.rating['ratingAttribute1'] === 'number'
  );
  return (
    reviewsWithRating.reduce((acc, review) => {
      let newValue = acc;
      if (typeof review.rating === 'object') {
        const rating = review.rating['ratingAttribute1'];
        if (rating) {
          newValue += rating;
        }
      }
      return newValue;
    }, 0.0) / reviewsWithRating.length
  );
};

Now, what’s annoying is that Typescript does not know that by running the reviews.filter function I type guarded the Reviews only to a subset of the Reviews that have rating of type ComplexType and also the ones that have ratingAttribute1: number; rather than ratingAttribute?: number.

What I’d love to end up with is not having to repeat the type checks in the calculation effectively ending up with:

const calculateAverageRating = (reviews: Review[]): number => {
  const reviewsWithRating = reviews.filter(
    (review) =>
      typeof review.rating === 'object' &&
      typeof review.rating['ratingAttribute1'] === 'number'
  );
  return (
    reviewsWithRating.reduce(
      (acc, review) => acc + review.rating['ratingAttribute1'],
      0.0
    ) / reviewsWithRating.length
  );
};

but that does not work out of the box:
enter image description here

Is there any way of achieving this level of type guarding? Or is there a neater of way doing this type of stuff?

Solution

The .filter() function will always return an array of the same type that was given as the argument. That is why reviewsWithRating is still a Review[], even after you filter it.

To change this, you can add a type guard to the callback:

const reviewsWithRating = reviews.filter(
  (review): review is { rating: Required<ComplexRating> } =>
    typeof review.rating === 'object' &&
    typeof review.rating['ratingAttribute1'] === 'number'
);

Now TypeScript will know that reviewsWithRating is of type { rating: Required<ComplexRating> }[].

Answered By – Tobias S.

Answer Checked By – Pedro (AngularFixing Volunteer)

Leave a Reply

Your email address will not be published.