Search code examples
typescript

In reduce() call Typescript sometimes infers type from accumulator and sometimes from array itself


I don't understand, why for reduce() with explicitly provided initial value of number 0, TS still infers accumulator type by array's type

const a: (number | undefined)[] = [1, 2, 3]

const b =  a.reduce((sum, el) => {
//                    ^? number | undefined
  if (typeof el === 'number') {
    return sum + el
//  Error!  ^ 'sum' is possibly 'undefined'.(18048)

  }
  return sum
}, 0)

I can specify type directly

const a: (number | undefined)[] = [1, 2, 3]

const b =  a.reduce<number>((sum, el) => {
//                            ^? number
  if (typeof el === 'number') {
    return sum + el
  }
  return sum
}, 0)

But why is it required here?

And even more confusing, if accumulator and array elements are of different types, TS infers correctly:

const a: (number | undefined)[] = [1, 2, 3]

const b =  a.reduce((sum, el) => {
//                    ^? string
  if (typeof el === 'number') {
    return sum + el
  }
  return sum
}, '')

What do I miss here?


Solution

  • See microsoft/TypeScript#42201 for an authoritative answer to this question.


    If you look at the TypeScript library typings for the reduce() method of arrays you'll see that there are two overloaded call signatures which accept an initialValue argument. They look more or less like this:

    interface Array<T> {
        reduce(cb: (prev: T, curr: T) => T, initialValue: T): T;
        reduce<U>(cb: (prev: U, curr: T) => U, initialValue: U): U;
    }
    

    When you call an overloaded function, TypeScript essentially tries each one in order, and the first one that matches is the one that it uses. That means if the compiler can treat initialValue as a T, it will try to do so. Only if that fails will it try to select the generic call signature and infer a different type for initialValue.

    So for

    const b = a.reduce((sum, el) => sum + (el ?? 0), 0); // error!
    // possibly undefined --------> ~~~
    

    the compiler checks 0 against number | undefined. It succeeds, so that's the selected call signature. It then contextually types sum and number | undefined and you get the error you ran into.

    Oops.


    Of course if you go ahead and manually specify a generic type argument for reduce(), then it will automatically select the generic call signature:

    const c = a.reduce<number>((sum, el) => sum + (el ?? 0), 0); // okay
    

    And if you specify an initialValue which is not assignable to number | undefined, then TypeScript will skip the first call signature and choose the second:

    const d = a.reduce((sum, el) => sum + (el ?? 0), ""); // okay, string
    

    That explains the behavior for your example.


    Of course this doesn't explain why those overloads are the way they are. Multiple people have run into problems with this, and it has been suggested to remove the non-generic call signature, or to put the generic one before the non-generic one. But these all cause other things to break and it wasn't pursued. See microsoft/TypeScript#25454 and microsoft/TypeScript#39259.

    At some point, microsoft/TypeScript#36554 was opened to collect use cases for array methods that are not served well by the current typings, with the idea that eventually there'd be a re-vamp of these method typings. And that's where the reduce() typings have been languishing for a while.

    Maybe eventually something will change. But for now this is how it is.

    Playground link to code