Antyos
Antyos

Reputation: 545

Typescript conditionally optional callback for specific generic

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

Answers (1)

jcalz
jcalz

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.

Playground link to code

Upvotes: 2

Related Questions