I'm writing a higher-order function, opposite(fn)
. This opposite
takes a boolean or numeric fn
function as input, and returns a new function, which should be of the same type as fn
. If the original function is of boolean-returning type, the new function returns "not that boolean". If the original function is of number-returning type, the new function returns 1 or 0 depending if the result was zero or not. You can use opposite
to invert a filter, from someArray.filter(testCondition)
to someArray.filter(opposite(testCondition))
.
The problem: both return
sentences produce an error Type 'number' is not assignable to type 'ReturnType<T>' ts(2322)
.
Why cannot I assign a boolean
or number
to a boolean|number
? How should I apply data types to my opposite
function?
const opposite =
< T extends
| ((...args: any[]) => boolean)
| ((...args: any[]) => number)
>(fn: T) =>
(...args: Parameters<T>): ReturnType<T> => {
const result = fn(...args);
if (typeof result === "number") {
return result === 0 ? 1 : 0;
} else {
return !result;
}
};
CodePudding user response:
I trip up on things like this every time I pick up TS again.
I don't think you can type this without partially turning off type checking one way or another.
Note that I have added the util Widen
because your function signature was wrong: if fn
is () => true as const
, then opposite(fn)
is () => true
instead of () => boolean
type Widen<T> = T extends number ? number : T extends boolean ? boolean : T;
const opposite =
< T extends
| ((...args: any[]) => boolean)
| ((...args: any[]) => number)
>(fn: T) =>
(...args: Parameters<T>): Widen<ReturnType<T>> => {
const result = fn(...args);
if (typeof result === "number") {
return result === 0 ? 1 : 0 as any;
// ------
} else {
return !result as any;
// ------
}
};
const opposite:{
<Args extends unknown[]>(f: (...args: Args) => boolean): (...args: Args) => boolean
<Args extends unknown[]>(f: (...args: Args) => number): (...args: Args) => 0 | 1
} = <Args extends unknown[]>(f: (...args: Args) => boolean | number) => (...args: Args): any => {
// ---
const result = f(...args);
if(typeof result === 'number') {
return result === 0 ? 1 : 0;
} else {
return !result;
}
};
type Widen<T> = T extends number ? number : T extends boolean ? boolean : T;
const opposite =
<Args extends unknown[], R extends boolean | number>
(f: (...args: Args) => R) => (...args: Args): Widen<R> => {
const result = f(...args);
if(typeof result === 'number') {
return result === 0 ? 1 : 0 as any;
// ------
} else {
return !result as any;
// ------
}
};
A few things can conspire for it not to compile:
- In your case I think the culprit is that
ReturnType<T>
is undetermined: it is notnumber | boolean
and the compiler won't compute the value. - In the case of the implementation with overloads, the problem is that overloads are an intersection, and the intersection
boolean & number
isnever
. - The problem with the last implementation is that
R
could be provided by the caller: If they pass in5
,boolean
no longer extendsR
.
I may be slightly wrong about those interpretations but this mental model works quite well and the bottom line is you are stuck.