Consider these three objects:
const obj = {
name: 'bob',
};
const obj2 = {
foo: 'bar',
};
const obj3 = {
fizz: 'buzz',
};
I've written a simple merge function that merges these three objects into one:
// suboptimal solution. Only takes in 3 params
const merge = <A extends Record<string, unknown>, B extends Record<string, unknown>, C extends Record<string, unknown>>(...rest: [A, B, C]) => {
return rest.reduce((acc, curr) => ({ ...acc, ...curr }), {}) as A & B & C;
};
// Totally works. IDE TypeScript support autocomplete works and the return type is exactly as
// expected. test1 type has properties of "foo", "name", and "fizz"
const test1 = merge(obj, obj2, obj3);
There are two problems with this:
- It only supports 3 objects at the moment. I want to support N number of objects.
- The generic is freaking huge
The return type of this function (observable by hovering over test1
) is an intersection of all the inferred types of the 3 objects passed in. Beautiful - exactly what I'm looking for, but the solution is still suboptimal.
I've tried doing this:
// Attempting to take in N number of objects instead of just supporting 3
const merge2 = <T extends Record<string, unknown>[]>(...rest: T) => {
// Merging them all
return rest.reduce((acc, curr) => ({ ...acc, ...curr }), {});
};
// Function obviously works, but test2 type is just "Record<string, unknown>". TypeScript doesn't seem to
// know that test2 has "name", "foo", and "fizz" properties
const test2 = merge2(obj, obj2, obj3)
The function of course is the same, but instead I'm trying to accept an arbitrary number of objects. The problem is that the return type the function is always Record<string, unknown>
, so the type inferences for the 3 (or more) objects passed in is totally lost. Therefore, you can't do test2.f
and get autocomplete for foo
.
How can I avoid losing the object types? And how can I created an intersection between N number of types?
CodePudding user response:
Here is a solution that uses the UnionToIntersection
type from here.
type UnionToIntersection<U> =
(U extends any ? (k: U)=>void : never) extends ((k: infer I)=>void) ? I : never
const merge = <
T extends any[]
>(...rest: T): UnionToIntersection<T[number]> => {
return rest.reduce((acc, curr) => ({ ...acc, ...curr }), {});
};
I used the generic type T
to store all the types passed as args
in an array. Using T[number]
we can get a union of all elements in the array T
. With the UnionToIntersection
type, we convert this union to an intersection which will be the return type of the function.
Let's see if it works:
const test = merge(obj, obj2, obj3)
// const test: {
// name: string;
// } & {
// foo: string;
// } & {
// fizz: string;
// }