简体   繁体   中英

Typescript reduce a list of objects to an object with a different shape

I cannot for the life of me work out how to modify the return value of reduce without a number of clunky type assertions. For example:

const list: Array<Record<string, string | number>> = [
  {
    resourceName: "a",
    usage: 20
  },
  {
    resourceName: "b",
    usage: 50
  }
];

const toMap = list.reduce(
  (acc, item) => ({
    ...acc,
    [item.resourceName]: item.usage
  }),
  {}
);

toMap has the type Record<string, string | number> Record<string, string | number> as list has the shape Array<Record<string, string | number>> Array<Record<string, string | number>> but I don't care about the shape of my input list, I care about the shape of my output object, which is Record<string, number> .

In this example I could add a type assertion to the end to get the right type but there are times when the list type and output type don't match and having to use as unknown as X for an operation as common as reduce seems incorrect to say the least.

You do need to care about the shape of the input list, at least to the extent that you want the compiler to understand that item.usage is a number and not a string | number string | number . If you annotate list as Array<Record<string, string | number>> Array<Record<string, string | number>> , then you're explicitly telling the compiler to forget everything about list except that it's an array of objects whose properties are of type string | number string | number . After this, any use of reduce() with item.usage would necessarily involve string | number string | number , unless you employ something like a type assertion to give the compiler back some of the information you threw away earlier.

In this case I'd recommend leaving off the type annotation for list and letting the compiler infer its type from the initialized value:

const list = [
  {
    resourceName: "a",
    usage: 20
  },
  {
    resourceName: "b",
    usage: 50
  }
];

/* const list: {
    resourceName: string;
    usage: number;
}[] */

You can see that this inferred type is an array of objects whose resourceName property is of type string and whose usage property is of type number . This is a more useful classification of list for your purposes.


Now you want to use reduce() . The issue here is that you are passing in an initial accumulator which is an empty object {} . Left to its own devices, the compiler will infer {} as the type of this initial accumulator, and from there infer that the generic type parameter on the reduce() call signature is also of type {} , and finally conclude that the return value is of type {} as well. (Sometimes the compiler's type inference gives you the types you want, and other times it doesn't. That's just the way it goes with heuristics.) If you want something else to happen, you need to prevent this inference.

One way to do it is to declare the initial accumulator ahead of time with its type explicitly annotated to what you want:

const init: Record<string, number> = {};
const toMap = list.reduce(
  (acc, item) => ({
    ...acc,
    [item.resourceName]: item.usage
  }),
  init
);
// const toMap: Record<string, number>

Now the compiler does not need to infer the type of init ; and the type parameter in reduce() is inferred to be Record<string, number> , and so toMap is also a Record<string, number> .

Another way to do it is to manually specify the type parameter when you call reduce() . This prevents the inference of the type parameter, and allows the compiler to contextually infer the type of your initial accumulator from it:

const toMap = list.reduce<Record<string, number>>(
  (acc, item) => ({
    ...acc,
    [item.resourceName]: item.usage
  }),
  {}
);
// const toMap: Record<string, number>

Either way should work for you.

Playground link to code

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM