const array: unknown[] = []
const a = array.map(() => '') // a is string
const b = array.reduce((acc) => acc, 'initial') // b is unknown (why?)
For the map
call above, TS inferred the following signature:
map<U>(callbackfn: (value: T, index: number,
array: T[]) => U, thisArg?: any): U[];
which makes sense (U
is string
, T
is unknown
).
But for the reduce
call, TS inferred:
reduce(callbackfn: (previousValue: T, currentValue: T,
currentIndex: number, array: T[]) => T, initialValue: T): T;
(generalizing the return type to T = unknown
) instead of inferring
// This signature is also available in lib.es5.d.ts
reduce<U>(callbackfn: (previousValue: U, currentValue: T,
currentIndex: number, array: T[]) => U, initialValue: U): U;
Why didn't TS narrowed the inferred types of the reduce
call to T = unknown
and U = string
?
A function/method with multiple call signatures is overloaded. When you call such a function, TypeScript resolves the function call by selecting one of the call signatures. Generally speaking it selects the first call signature that matches the arguments. That is, the call signatures in overloads are ordered and the order of overloads affects which call signature has higher priority.
The typings for Array<T>.reduce()
in the TS standard library look like
reduce(cb: (prev: T, curr: T, idx: number, arr: T[]) => T): T;
reduce(cb: (prev: T, curr: T, idx: number, arr: T[]) => T, init: T): T;
reduce<U>(cb: (prev: U, curr: T, idx: number, arr: T[]) => U, init: U): U;
If you call reduce()
with two arguments then the first overload cannot succeed, so it has to choose either the second or the third. If the type of init
is assignable to T
, then the second overload will succeed and is chosen. In your case T
is the unknown
type, to which everything is assignable, so it pretty much guarantees that the second call signature is selected. The third, generic, call signature is not even tried.
So that's why you see this behavior. The map()
method typings on the other hand have just one call signature, and it's generic.
If you want to call reduce()
so that the third call signature is chosen, you need to change how you call it so that it's not compatible with the first two call signatures. The easiest way to do this is to just specify a generic type argument, since neither of the first two call signatures declares a generic type parameter:
const b = array.reduce<string>((acc) => acc, 'initial') // string
There are other approaches, like manually annotating the prev
callback parameter so that T
is not assignable to it, since functions are contravariant in their parameter types (see Difference between Variance, Covariance, Contravariance, Bivariance and Invariance in TypeScript), and thus narrowing prev
widens the type of cb
so it no longer matches the second overload:
const c = array.reduce((acc: string) => acc, 'initial')
But these workarounds are outside the scope of the question as asked so I'll stop here.