If you want a non-recursive way to get an intersection of all the elements of a tuple type, you can do it like this:
type TupleToIntersection<T extends readonly any[]> =
{ [N in keyof T]: (x: T[N]) => void }[number] extends
(x: infer I) => void ? I : never;
It works via some type juggling that is easier to write than to explain (but I'll try): First we map the tuple T
to a new version where each element at numeric index N
(that is, T[N]
) has been placed in a contravariant position (as the parameter to a function, (x: T[N]) => void
), and then index into that tuple with number
to get a union of those functions. From this we use conditional type inference to infer a single function parameter type for that union of functions, which (as mentioned in the linked doc) produces an intersection I
.
Whether that makes sense or not, you can observe that it works:
type ABC = TupleToIntersection<[A, B, C]>
// type ABC = A & B & C
type AorBandC = TupleToIntersection<[A | B, C]>
// type AorBandC = (A | B) & C
Note that the compiler preserves that A | B
union in the second example.
Anyway that means we can write the output of merge()
as TupleToIntersection<T>
:
function merge<T extends any[]>(...args: T): TupleToIntersection<T> {
return args.reduce((previous, current) => {
return Object.assign(previous, current);
});
}
And it produces the expected output:
const m = merge(a, b, c);
// const m: A & B & C
m.a;
m.b;
m.c;
const m2 = merge(Math.random() < 0.5 ? a : b, c)
// const m2: (A | B) & C
Playground link to code