Home > Enterprise >  How to type object aggregation function in TypeScript
How to type object aggregation function in TypeScript

Time:08-10

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.

CodePudding user response:

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

  • Related