how does Typescript determine the type of the array.reduce() return value?

Viewed 812

How does Typescript (v4.0.2) decide what should be the return type of the array.reduce() function ?

(Note: I mean the reduce() function itself, not the callback function passed to the reduce() function!)

Reason to ask is that I always find a workaround to get rid of TS errors, but never understand why the problems exist. I have seen several questions and solutions, but they all only suggest some solution for specific situations, more or less with insufficient explaination (type-safe-array-reduce, type-safe-way-to-reduce-a-larger-object-into-a-new-type, ...).

Specifically, in the below example, why does TS think that the reduce() function returns IOldType, when the reducerFunction() returns INewType[] ?

Example:

// -- interfaces (no error here) --

interface IOldType {
    [key: string]: any
}

interface INewType {
    propA: string
    propB: number
    propC: boolean
}

// -- data (no error here) --

const initialValue: INewType[] = [];

const oldData: IOldType[] = [
    { id: 'item-01', b: true, c: null },
    { id: 'item-02', b: false, otherProp: null },
    { id: 'item-03', b: false },
    { id: 'item-04' },
    {},
];

// -- reducer function (no error here) --

const reducerFunction = ( acc: INewType[], item: IOldType): INewType[] => {

    if( !item.b || !item.id ){ return acc; }

    const newItem: INewType = {
        propA: item.id,
        propB: 0,
        propC: item.b
    };

    return [ ...acc, newItem ];   // <-- (FYI: 'as INewType[]' here doesn't make a difference)
};

// -- calling .reduce() --> TS error --

const newData: INewType[] = oldData.reduce( reducerFunction, initialValue ); // <-- ERROR "TS2740: Type 'IOldType' is missing the following properties from type 'INewType[]': length, pop, push, concat, and 16 more."

// --------

This throws the error "TS2740: Type 'IOldType' is missing the following properties from type 'INewType[]': length, pop, push, concat, and 16 more." or "Initializer type IOldType is not assignable to variable type INewType[]" (depending on the configuration, apparently. I think the main problem is the same.).

The workarounds I don't like

To make it work, I can do different workarounds, but they all seem to not really solve the problem:

1. I can add (the often suggested) as INewType[]:

const newData: INewType[] = oldData.reduce( reducerFunction, initialValue ) as INewType[];

But that feels like 'cheating'. I might return something wrong and insist that it is INewType[]. I can even write:

const newData: INewType[] = [1,2,3].reduce( (a, i) => [1], [] ) as INewType[];

2. I can define an extra, unneeded optional key in the interface. I have no idea why this makes it work:

interface IOldType {
    anyKey?: string  // <-- I don't need or want this, but it works if I add it
    [key: string]: any
}

3. I can omit the type definition of the accumulator and the return type of the reducer (or define 'any'), but I suppose it should always be better to define the correct types instead of any (?):

const reducerFunction = ( acc, item: IOldType ) => {
    // ...
}
0 Answers
Related