Given a box:
type A = {readonly id: 'A'}
type B = {readonly id: 'B'}
type Value = A | B
class Box<T extends Value | {[x: string]: Value}> {
constructor(public value: T) {}
}
I would like to create a function merge:
let a = {id: 'A'} as A
let b = {id: 'B'} as B
let merged = merge(
new Box({position: a}),
{velocity: new Box(a), color: new Box(b)},
{mass: new Box(b)}
)
Such that the type of merged is Box<{position: A, velocity: A, color: B, mass: B}>
Here's what I came up with:
// {position: Box<A>} => {position: A}
// Box<{position: A}> => {position: A}
type InferBoxValue<B extends {[x: string]: Box<Value>} | Box<{[x: string]: Value}>> =
B extends {[x: string]: Box<Value>} ? {[K in keyof B]: B[K] extends Box<infer F> ? F : unknown} :
B extends Box<infer F> ? F :
unknown
// [{color: Box<A>}, Box<{mass: B}>] => [{color: A}, {mass: B}]
type InferMultipleBoxValues<A extends any[]> = {
[I in keyof A]: A[I] extends {[x: string]: Box<Value>} | Box<{[x: string]: Value}>
? InferBoxValue<A[I]>
: unknown
}
// MergeTwo<{color: A}, {mass: B}> => {color: A, mass: B}
type MergeTwo<A, B> = {
[K in keyof (A & B)]: K extends keyof B ? B[K] : K extends keyof A ? A[K] : unknown
}
// Merge<[{color: A}, {mass: B}, ...]> => {color: A, mass: B, ...}
type Merge<A extends readonly {[x: string]: Value}[]> = A extends [infer L, ...infer R] ?
R extends {[x: string]: Value}[] ? MergeTwo<L, Merge<R>> : unknown : unknown
function merge<
B extends ({[x: string]: Box<Value>} | Box<{[x: string]: Value}>)[],
M extends Merge<InferMultipleBoxValues<B>>
>(...boxes: B): M extends {[x: string]: Value} ? Box<M> : unknown {
return null as any // implementation is trivial
}
This works to a degree. When I hover over merged within VSCode I see this type:
Box<MergeTwo<{position: A}, MergeTwo<{velocity: A; color: B}, MergeTwo<{mass: B}, unknown>>>>
I can tell that's equivalent to the type I want, since this works:
let test: Box<{position: A, velocity: A, color: B, mass: B}> = null as unknown as (
Box<MergeTwo<{position: A}, MergeTwo<{velocity: A; color: B}, MergeTwo<{mass: B}, unknown>>>>
)
But that's not quite what I'm looking for. I do not want users of Box to see any of InferBoxValue, InferMultipleBoxValues, MergeTwo or Merge. How can I adjust my implementation of merge so TypeScript displays the results correctly?
Thanks for taking a look at this!
Using TypeScript v4.6.2 (Playground Link)