Home > Software engineering >  TypeGuards for optional property while filtering array of objects
TypeGuards for optional property while filtering array of objects

Time:05-19

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?

CodePudding user response:

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> }[].

  • Related