1

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?

1 Answer 1

2

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

Sign up to request clarification or add additional context in comments.

2 Comments

XO gives the following error on the return statement: "Unsafe return of an any typed value. (@typescript-eslint/no-unsafe-return)" Any idea how to fix this? This is the first time I'm using XO--I thought it was a "plug and play" type linter/formatter, but it seems like it can be a pain which is a bit disappointing.
I was able to fix my issue with TS2349 by replacing the two iter.reduce() with (iter as T[]).reduce() and (iter as number[]).reduce(), respectively. Could you edit the answer so other people may see the solution?

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.