Home > Enterprise >  Array.reduce gives wrong result type
Array.reduce gives wrong result type

Time:10-08

I found a very surprising behavior of Typescript's type-inference and suspect if it is a bug.

Suppose I have a list of MyItem interface.

interface MyItem {
    id?: string;
    value: string;
}
const myItemList: MyItem[] = [];

Please notice that the MyItem.id is optional while MyItem.value is mandatory.

I want to use method Array.reduce on myList, like this

const myMap = myItemList.reduce((accum, item) => ({ ...accum, [item.id!]: item }), {});

This will make the result variable myMap to be of type {}, which is as expected because I supplies {} as the initial value in myItemList.reduce(...), as shown in the picture below.

enter image description here

Now, the problem occurs when I modify the MyItem interface so that the value becomes optional.

interface MyItem {
    id?: string;
    value?: string; // << here I make it optional
}
const myItemList: MyItem[] = [];

Surprisingly, the exact same myItemList.reduce(...) statement yields a result of MyItem instead of {}.

Is this a bug? If not, please help me understand what is the reason behind it.

enter image description here

CodePudding user response:

Not a TS expert, but this doesn't seem too surprising to me: if both fields of the interface are optional, then any empty object is potentially a valid MyItem instance. So when the system sees an empty object and tries tries to infer a type for it, MyItem is a match.

I'm assuming that TS defines an "optional" field as including "undefined/absent" and not "existent, but potentially holding undefined or null", which would be closer to a Nullable<T> field in, for example, C#/.NET. Considering it's non-trivial the tell the two cases apart in JS*, this seems like the more reasonable definition if you're coming from a JS background.


* You'd have to use prop in obj; obj.prop === undefined for both obj = {} and obj = {prop: undefined}, even in strict mode(!)

CodePudding user response:

Thanks to a hint in @solarshado's answer, I have investigated further and found that this is not the wrong type-inference. It is the expected behavior of method overloading resolution.

Javascript's Array has 2 overloading methods with signature matched the parameters in the statement myItemList.reduce((accum, item) => ({ ...accum, [item.id!]: item }), {});

Which are

1. reduce(callbackfn: (previousValue: T, currentValue: T, currentIndex: number, array: T[]) => T, initialValue: T): T;
2. reduce<U>(callbackfn: (previousValue: U, currentValue: T, currentIndex: number, array: T[]) => U, initialValue: U): U;

It surprised me because the expectation in my mind is that I am using the second overload (reduce<U>(...)). But actually, the compiler used the first overload for some reason (maybe related to the overload resolution algorithm).

Therefore my problem should be solved by providing more information to the compiler to pick up the correct overload.

E.g.

const myMap = myItemList.reduce<{}>((accum, item) => ({ ...accum, [item.id!]: item }), {});

Or the more specific one

const myMap = myItemList.reduce<{[id: string]: MyItem}>((accum, item) => ({ ...accum, [item.id!]: item }), {});
  • Related