I'm trying to create a utility function that combines filter and map in a single traversal. Given an array of items, a filter function, and a mapping function, I want to return the filtered and mapped values with the types working as expected. I wrote something like this:
function filterMap<A extends B, B, C>(
items: A[],
filter: (item: A | B) => boolean,
mapper: (item: B) => C
) {
function wrappedFilter(item: A | B): item is B {
return filter(item);
}
const result: C[] = [];
for (const item of items) {
if (wrappedFilter(item)) {
const filtered: B = item;
result.push(mapper(filtered));
}
}
return result;
}
const items = [1, 2, undefined, 4, undefined, undefined, 7];
const isDefined = (n: number | undefined) => n !== undefined;
const double = (n: number) => n * 2;
filterMap(items, isDefined, double);
But if you look at this code in TS playground (or your preferred IDE) you'll see an error on items
. In this case number | undefined
is not assignable to number
. What am I doing wrong? How can I keep this function generic?
Since the contents of items
are of type A
, and you are trying to use filter
as a type guard function to narrow from A
to B
, then you want B
to extend A
and not vice versa. That means you should move your constraint from A
to B
:
function filterMap<A, B extends A, C>(
items: A[],
filter: (item: A) => boolean,
mapper: (item: B) => C
) {
function wrappedFilter(item: A): item is B {
return filter(item);
}
const result: C[] = [];
for (const item of items) {
if (wrappedFilter(item)) {
const filtered: B = item;
result.push(mapper(filtered));
}
}
return result;
}
That also means that A | B
is effectively just A
(by absorption), so I've made that change as well.
Now your code works as intended:
filterMap(items, isDefined, double); // okay