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?
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.