2

According to some business rules, I created an aggregation function that I need to type correctly.

The function (JS):

It takes:

  • Initial object.
  • n objects.
export function aggregateObjects(initialObject, ...newObjects) {
  const aggregate = (objectA, objectB) => {
    return Object.keys(objectB).reduce(
      (acc, key) => ({
        ...acc,
        [key]: [...(acc[key] ?? []), ...(objectB[key] ?? [])],
      }),
      objectA
    );
  };

  return newObjects.reduce(
    (aggregatedObjects, newObject) => aggregate(aggregatedObjects, newObject),
    initialObject
  );
}

How to use it (TypeScript):

Here is an example of two new objects passed to aggregateObjects, but it should work with 5, 10, ...

const initialObject = {
  a: [objectOfType1],
};

const newObject1 = {
  a: [objectOfType2],
  b: [objectOfType1],
};

const newObject2 = {
  c: [objectOfType1, objectOfType2],
};

const result = aggregateObjects(initialObject, [newObject1, newObject2]);

What we expect (TypeScript):

According to previous example, we expect this result:

  • a should be typed as an array of ObjectType1 or ObjectType2.
  • b should be typed as an array of ObjectType1.
  • c should be typed as an array of ObjectType1 or ObjectType2.
type Result = {
  a: (ObjectType1 | ObjectType2)[];
  b: ObjectType1[];
  c: (ObjectType1 | ObjectType2)[];
};

What I tried (TypeScript):

Of course, it does not work:

  • Internal aggregate seems OK (even if there's some issues related to spread).
  • Outside, I didn't find a solution to correctly type return value.

Note that CombineObjs is a TS helper inspired from https://dev.to/svehla/typescript-how-to-deep-merge-170c

export function aggregateObjects<T extends object, U extends any[]>(
  initialObjects: T,
  ...newObjects: U
) {
  const aggregate = <A extends object, B extends object>(
    objectA: A,
    objectB: B
  ): CombineObjs<A, B> => {
    return (Object.keys(objectB) as Array<keyof B>).reduce(
      (acc, key) => ({
        ...acc,
        [key]: [
          // @ts-ignore
          ...(acc[key as keyof CombineObjs<A, B>] ?? []),
          // @ts-ignore
          ...(objectB[key] ?? []),
        ],
      }),
      objectA as unknown as CombineObjs<A, B>
    );
  };

  return newObjects.reduce(
    (aggregatedObjects, newObject) => aggregate(aggregatedObjects, newObject),
    initialObjects
  );
}

Thanks for your solutions.

8
  • Why should c be of type ObjectType2[] and not (ObjectType1 | ObjectType2)[]? It would be helpful if at least the JavaScript part was working before trying to help you with the typing. Commented Aug 9, 2022 at 10:20
  • It was a mistake; it's fixed now. Commented Aug 9, 2022 at 10:24
  • Also you mention "recursivity". But I don't see any recursive function call. Commented Aug 9, 2022 at 10:25
  • You're right: there's not recursion inside my implementation. I use a CombineObjs TS util taking only two generics, and I need to have it working on n generics. I didn't find a solution. Finally, the return type I want is a combine of multiple objects (but I can't use this type). Commented Aug 9, 2022 at 10:31
  • Is there some constraint to the ObjectTypes? Are they always supposed to be a flat object structure where the values are tuples? Or can they have an arbitrary shape? What would happen if a: "123"? Commented Aug 9, 2022 at 10:31

1 Answer 1

3

Here is the typing for the function.

type UnionToIntersection<U> =
  (U extends any ? (k: U) => void : never) extends ((k: infer I) => void) ? I : never;

function aggregateObjects<
  T extends Record<string, any[]
>[]>(objects: [...T]): { 
  [K in keyof UnionToIntersection<T[number]>]: (T[number] extends infer U ? 
    U extends Record<K, (infer V)[]> 
      ? V
      : never
    : never)[]
} {
    return {} as any
}

I did not see any reason for the initialObject to be a separate parameter, since it seems to be handled exactly like the other objects.

const initialObject = {
  a: ["objectOfType1"],
};

const newObject1 = {
  a: [23],
  b: ["objectOfType1"],
};

const newObject2 = {
  c: ["objectOfType1", 23],
};

const result = aggregateObjects([initialObject, newObject1, newObject2]);
// const result: {
//   a: (string | number)[];
//   b: string[];
//   c: (string | number)[];
// }

Playground

Sign up to request clarification or add additional context in comments.

2 Comments

Thanks a lot for your answer and your time, I'm going to try that. I agree that it's not useful to provide separate initial object.
For information, here is my working version, with updated implementation types: stackblitz.com/edit/typescript-spwu7a?file=index.ts

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.