Reputation: 1360
I am trying to combine the array methods filter and map into one function called malter. I have come so far:
type mapFn<T, S> = (value: T, index: number, originalArray: Readonly<T[]>) => S;
interface Array<T> {
malter<S = any>(mapFn: mapFn<T, S>): S[];
}
function notUndefined<T>(v: T | undefined): v is T {
return typeof v !== "undefined"
}
Array.prototype.malter = function malter<T, S = any>(mapFn: mapFn<T, S>): S[] {
return this.reduce(function(acc: S[], val: T, index: number, orgArr: T[]) {
const el = mapFn(val, index, orgArr);
if (notUndefined(el)) {
acc.push(el);
}
return acc;
}, []);
};
It basically works. But when using it, it throws a TypeError in line 5-7. Another test function that did implicitly return undefined did also throw that error.
Type 'string | undefined' is not assignable to type 'string'.
Type 'undefined' is not assignable to type 'string'."
const test = [2, 3, 4, 5, 3];
function test1(): string[] {
return test.malter<number, string>(num =>
num > 3
? num.toFixed(2)
: undefined
);
}
A solution that would work type savely, is to provide 2 arguments for malter. A filter and a map function and call them seperately. That would ensure type savety, but also make it less simple.
Sure I could simply do that in line 5-7:
(num > 3 ? num.toFixed(2) : undefined) as string
It may be the best compromise? What do you think? Is there a solution I didn't think of, or do you go with a compromise?
Upvotes: 1
Views: 316
Reputation: 327754
@TitianCernicova-Dragomir's answer is correct, but I'd also like to give a slightly different solution. The main difference is that instead of using conditional types to turn S
(which possibly includes undefined
) into to Exclude<S, undefined>
(which doesn't), we take S
as the return type (which doesn't include undefined
) and use the input type as S | undefined
. They will act the same (or nearly so) from the caller's point of view (the compiler will do its own Exclude
-like analysis on the input type), but the compiler will likely be able to reason about types inside the implementation of malter
better in the latter case:
type mapFn<T, S> = (value: T, index: number, originalArray: Readonly<T[]>) => S;
interface Array<T> {
// S will not include undefined when inferred from mapFn
malter<S>(mapFn: mapFn<T, S | undefined>): S[];
}
// hey, look, this works the same way, with T|undefined => T
// instead of T => Exclude<T, undefined>
function notUndefined<T>(v: T | undefined): v is T {
return typeof v !== "undefined";
}
Array.prototype.malter = function malter<T, S>(
this: Array<T>, // inform the compiler that this is an array
mapFn: mapFn<T, S | undefined>
): S[] {
return this.reduce(function(acc: S[], val: T, index: number, orgArr: T[]) {
const el = mapFn(val, index, orgArr);
if (notUndefined(el)) {
acc.push(el);
}
return acc;
}, []);
}; // inference in implementation works
const test = [2, 3, 4, 5, 3];
// don't need any type parameters
function test1(): string[] {
return test.malter(num => (num > 3 ? num.toFixed(2) : undefined));
}
Okay, hope that helps. Good luck!
Upvotes: 2
Reputation: 249486
You are using undefined
as the value to be filtered. We can allow the inner function to return undefined
and then filter it out using Exclude
, much like the implementation does:
type mapFn<T, S> = (value: T, index: number, originalArray: Readonly<T[]>) => S;
interface Array<T> {
malter<S>(mapFn: mapFn<T, S>): Exclude<S, undefined>[];
}
function notUndefined<T>(v: T | undefined): v is T {
return typeof v !== "undefined"
}
Array.prototype.malter = function malter<T, S>(mapFn: mapFn<T, S>): Exclude<S, undefined>[] {
return this.reduce(function (acc: S[], val: T, index: number, orgArr: T[]) {
const el = mapFn(val, index, orgArr);
if (notUndefined(el)) {
acc.push(el);
}
return acc;
}, []);
};
const test = [2, 3, 4, 5, 3];
function test1(): string[] {
return test.malter(num =>
num > 3
? num.toFixed(2)
: undefined
);
}
Upvotes: 1