如何让 TypeScript 正确显示此类型?(带有泛型的递归类型)



给定一个框:

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) {}
}

我想创建一个函数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)}
)

这样merged的类型就Box<{position: A, velocity: A, color: B, mass: B}>

这是我想到的:

// {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
}

这在一定程度上是有效的。当我将鼠标悬停在VSCode中的merged上时,我看到此类型:

Box<MergeTwo<{position: A}, MergeTwo<{velocity: A; color: B}, MergeTwo<{mass: B}, unknown>>>>

我可以说这相当于我想要的类型,因为这有效:

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>>>>
)

但这不是我想要的。我不希望Box用户看到任何InferBoxValueInferMultipleBoxValuesMergeTwoMerge。如何调整merge的实现,以便 TypeScript 正确显示结果?

感谢您查看此内容!

使用 TypeScript v4.6.2 (Playground 链接)

实际上@ashtonsix,可以做得比这更好!

有了这个有趣的类型:

type Expand<T> = T extends {} ? { [K in keyof T]: Expand<T[K]> } & {} : T;

它的工作方式是它显式地将类型中的所有内容提取到映射类型中,这意味着它显示为"对象文本"。如果类型是基元,则它不执行任何操作,并且此类型也会递归扩展。

让我们测试一下:

type Target = Box<MergeTwo<{position: A}, MergeTwo<{velocity: A; color: B}, MergeTwo<{mass: B}, unknown>>>>;
type Expansion = Expand<Target>;

编辑:哦,我刚刚注意到您希望它像Box<...>一样,考虑到它只是一个带有键的对象,这是可以理解的。我想你可以通过以下方式轻松补救:

type ExpandBox<T extends Box<{}>> = Box<Expand<T>["value"]>;
type Expansion = ExpandBox<Target>;

然后扩展上述Target将导致

操场

大部分功劳都归功于卡斯

我找到了解决方案。由于我们不希望MergeTwo出现在我们的类型定义中,我们可以简单地内联它。这是我们对Merge的新实现:

type Merge<Q extends readonly {[x: string]: Value}[]> =
Q extends [] ? {} :
Q extends [infer A] ? A :
Q extends [infer A, infer B] ? {[K in keyof (A & B)]: K extends keyof B ? B[K] : K extends keyof A ? A[K] : unknown} :
Q extends [infer A, infer B, infer C] ? {[K in keyof (A & B & C)]: K extends keyof C ? C[K] : K extends keyof B ? B[K] : K extends keyof A ? A[K] : unknown} :
Q extends [infer A, infer B, infer C, infer D] ? {[K in keyof (A & B & C & D)]: K extends keyof D ? D[K] : K extends keyof C ? C[K] : K extends keyof B ? B[K] : K extends keyof A ? A[K] : unknown} :
Q extends [infer A, infer B, infer C, infer D, infer E] ? {[K in keyof (A & B & C & D & E)]: K extends keyof E ? E[K] : K extends keyof D ? D[K] : K extends keyof C ? C[K] : K extends keyof B ? B[K] : K extends keyof A ? A[K] : unknown} :
Q extends [infer A, infer B, infer C, infer D, infer E, infer F] ? {[K in keyof (A & B & C & D & E & F)]: K extends keyof F ? F[K] : K extends keyof E ? E[K] : K extends keyof D ? D[K] : K extends keyof C ? C[K] : K extends keyof B ? B[K] : K extends keyof A ? A[K] : unknown} :
Q extends [infer L, ...infer R] ? R extends {[x: string]: Value}[] ? MergeTwo<L, Merge<R>> : unknown :
unknown

我们现在观察到merged的类型Box<{position: A, velocity: A, color: B, mass: B}>

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)}
)

merge现在在使用 <= 6 个参数调用时创建漂亮的类型,并在使用更多参数调用时回退到生成正确的类型。

游乐场链接

最新更新