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