Reputation: 545
I know there are numerous issues on StackOverflow about conditionally optional types in TypeScript, but none answered my question.
I have the following sum()
function that takes an array and a callback. The callback should be optional only if the array is number[]
.
function sum<T>(iter: T[], callback?: (arg: T) => number): number {
return callback
? iter.reduce((sum, element) => sum + callback(element), 0)
: iter.reduce((sum, x) => sum + Number(x), 0); // <- I want to get rid of Number()
}
While this code works, it implies that any T
could be converted to a number (which I don't want), while when T
is a number, it does a redundant cast.
My best attempt at fixing this with overloads is:
type OptionalIfNumber<T, U> = T extends number ? U | undefined : U;
export function sum(iter: number[], callback?: undefined): number;
export function sum<T>(iter: T[], callback: (arg: T) => number): number;
export function sum<T>(iter: T[], callback: OptionalIfNumber<T, (arg: T) => number>): number {
return callback
? iter.reduce((sum, element) => sum + callback(element), 0)
: iter.reduce((sum, x) => sum + x, 0); // <- Problem is here
}
It seems that TypeScript does not know that the only time callback
is undefined
, T
must extend number
.
Is what I'm trying to do possible?
Upvotes: 1
Views: 159
Reputation: 329258
The only way I know of to get the compiler to follow your logic is to make the function take a rest parameter of a union of tuple types, which is immediately destructured.
This lets the compiler see the [iter, callback]
pair as a destructured discriminated union where callback
is the discriminant property. Like this:
function sum<T>(...[iter, callback]:
[iter: number[], callback?: undefined] |
[iter: T[], callback: (arg: T) => number]
): number {
return callback ?
iter.reduce((sum, element) => sum + callback(element), 0) // okay
: iter.reduce((sum, x) => sum + x, 0); // okay
}
The above call signature says that the [iter, callback]
pair is either of type [number, undefined?]
or of type [T[], (arg: T)=>number]
. Inside the implementation, when callback
is truthy, the compiler automatically narrows iter
to T[]
and callback
to (arg: T)=>number
. Otherwise, it narrows iter
to number[]
and callback
to undefined
. So your conditional expression works in both cases.
When you call it, IntelliSense displays it like an overloaded function:
sum([4, 3, 2]);
// 1/2 sum(iter: number[], callback?: undefined): number;
sum(["a", "bc", "def"], x => x.length);
// 2/2 sum(iter: string[], callback: (arg: string) => number): number
So it doesn't change very much from the caller's side, either.
Upvotes: 2