0

Is it possible to two reduce objects into one by summing their properties like so for any generic object

const A = {x:1, y:1, z:1}
const B = {x:2, y:2, z:2}
// result {x:3, y:3, z:3}

I am hoping to get some function

function reduceObjects<T extends {[readonly key: string]: number}>(previousObject:T, currentObject:T) => T

when I try this solution

function reduceObjectsGeneric<T extends {readonly [key: string]: number}>(currentValue: T , previousValue: T): T {
  const result = Object.assign({}, previousValue);


  Object.keys(previousValue).forEach((k) => {
    // eslint-disable-next-line functional/immutable-data
    result[k]=previousValue[k]+currentValue[k]
  })

  return result
}

I get the following error in the inner loop

Type 'string' cannot be used to index type '{} & T'.ts(2536)

What is the functional way to implement this behaviour?

  • 2
    Welcome to Stack Overflow! Please take the [tour] (you get a badge!) and read through the [help], in particular [*How do I ask a good question?*](/help/how-to-ask) Your best bet here is to do your research, [search](/help/searching) for related topics on SO, and give it a go. ***If*** you get stuck and can't get unstuck after doing more research and searching, post a [mcve] of your attempt and say specifically where you're stuck. People will be glad to help. – T.J. Crowder Jun 02 '21 at 17:32
  • Thanks for adding your attempt. I see you have a line there to suppress an eslint error. Are you also happy to use type assertions, or trying hard to *avoid* type assertions? (I think of the latter as best practice, though for an isolated function like this I'd probably be happy enough to use them.) – T.J. Crowder Jun 02 '21 at 18:06
  • I am trying to avoid type assertions as I’d like to keep this function generic for any object with essentially all of its properties as numbers ( or anything that implements the + operator ) – Mohamad Abdel Rida Jun 04 '21 at 01:13
  • 1
    Binary `+` (the `+` that takes two operands) is "defined" for any type in JavaScript, but coerces its operands to either numbers or strings; the result is always a number or string. That's baked into the language (and not going to change). [This](https://tsplay.dev/mplagm) is the closest I could get in the time I could spend on it, and still needs two more type assertions (making three in total) to make it work. (the first type assertion is reasonable to me.) So I asked [this question](https://stackoverflow.com/questions/67834191/) about why the second and third assertions would be needed. – T.J. Crowder Jun 04 '21 at 09:59
  • Can we please have this question reopened? I am not sure who closed this as I have clearly narrowed the question down as much as a I can and I have provided a reasonable amount of thought. This is quite unfair and discouraging for people who are new to the platform. – Mohamad Abdel Rida Jun 04 '21 at 19:41
  • You forgot to specify the `extends {[key: string]: number}` in your attempt? – Bergi Jun 04 '21 at 20:01

3 Answers3

1

A functional approach would be (but probably not clean)

function reduceObjects<T extends { [key: string]: number }>(a: T, b: T): T {
  return Object.keys(a).reduce(
    (acc, key) => Object.assign(acc, { [key]: a[key] + b[key] }),
    b
  );
}

First, you get the keys of object "a" using Object.keys. Then you use the Array.reduce method of JavaScript to iterate over the keys and assign it to the "key" of the "acc" object.

Akshit Garg
  • 181
  • 2
  • 3
1

A generic reduceWith could provide the necessary scaffolding:


type ReduceObjectsWith = <
  V extends unknown, T = Record<PropertyKey, V>
>(fn: (acc: T, pair: [PropertyKey, V]) => T) => (...objs: T[]) => T;

type ReduceWithSum = ReduceObjectsWith<number>

const reduceObjectsWith = (fn) => (...objs) => objs.reduce(
  (res, curr) => Object.entries(curr).reduce(fn, res), 
  {},
);

const reduceWithSum = reduceObjectsWith(
  (res, [key, value]) => ({ 
    ...res, 
    [key]: value + (res[key] ?? 0),
  }),
);

console.log(
  reduceWithSum(
    {x: 1, y: 1, z: 1},
    {x: 2, y: 2, z: 2},
    {x: -2, y: 2, z: 5},
    {x: -1, y: 0, z: 32},
  ),
);
Hitmands
  • 13,491
  • 4
  • 34
  • 69
0

Another approach that uses a read-only functional approach that passes most es-lint rules:

function reducer<T extends {readonly [key: string]: unknown|number}>(object1: Partial<T>, object2: Partial<T>): T {
  return Array.from(new Set([...Object.keys(object1),...Object.keys(object2)]).values()).reduce(
    (p: T, c: string) => {
      if ((object1[c] instanceof Object)) {
        return { [c]: reducer(object1[c] as Partial<T>, (object2[c] ?? {}) as Partial<T>),...p}
      }
      const v1 = object1[c]??ZERO
      const v2 = object2[c]??ZERO
      return {[c]: (v1 as number)+(v2 as number),...p,}
    }, {} as T
  )
}