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 ) => {
// ...
}