我只考虑类型级别的转换,而不考虑类型inference或任何类型的implementation的问题.我在这里的目标是编写一个实用程序类型toTree<T>
,以便在给定如下所示的"平面"数据 struct 时
type Flat = {
key1: { property: "value1" },
key2: { property: "value2" },
key3: { property: "value3", parentKey: "key2" },
key4: { property: "value4", parentKey: "key3" }
};
转换后的类型ToTree<Flat>
是对应的树 struct :
type Tree = ToTree<Flat>;
/* type Tree = {
key1: { property: "value1"; };
key2: {
property: "value2";
key3: {
property: "value3";
key4: { property: "value4"; };
};
};
*/
这必然会是recursive conditional type,这往往会有有趣的边缘情况.如果递归条件类型的边缘情况行为不是我们所希望的,那么有时需要进行重大重构才能解决这个问题.因此,请注意.
我的方法是这样的.首先,我将编写一个中间实用程序类型ToTreeInner<T>
,它假定T
中的每个属性都有一个parentKey
属性,该属性可以是键,也可以是undefined
.因此,它需要{ key1: { property: "value1", parentKey: undefined }}
个,而不是{ key1: { property: "value1" }}
个.这使得它的实现更加简单,因为我们总是可以只获取parentKey
属性,而不必每次都编写处理丢失键的逻辑.如果我们有ToTreeInner
,我们可以这样写ToTree
:
type ToTree<T extends Record<keyof T, {
parentKey?: PropertyKey, [k: string]: any
}>> = ToTreeInner<{ [K in keyof T]: T[K] & (
"parentKey" extends keyof T[K] ? unknown : { parentKey: undefined }
) }>;
这允许T
到optionally的每个属性都有parentKey
属性(我添加了一个通用的index signature,这样就不会碰到weak type detection).然后,它将parentKey
的值加到任何不具有此类属性的属性上,然后将其传递给ToTreeInner
.
因此,现在让我们实现ToTreeInner
:
type ToTreeInner<
T extends Record<keyof T, { parentKey: PropertyKey | undefined }>,
P extends PropertyKey | undefined = undefined
> = { [K in keyof T as P extends T[K]["parentKey"] ? K : never]:
(Omit<T[K], "parentKey"> & ToTreeInner<T, K>) extends
infer O ? { [K in keyof O]: O[K] } : never
};
此类型函数接受两个类型参数.类型T
是全平面 struct ,而P
是输出树中当前 node 的父键的名称.如果我们在树的根上,那么它可以是undefined
,它是defaults到undefined
,所以您可以省略那个类型参数来获得完整的树.
然后我们有一个key-remapped type { [K in keyof T as P extends T[K]["parentKey"] ? K : never]: ⋯ }
,它使用as
子句来筛选键.如果P extends T[K]["parentKey"]
,则输出对象将仅具有键K
,这意味着如果T
中的相应属性具有parentKey
为P
,则输出对象将仅具有键K
.
并且关键字K
处的值基本上是Omit<T[K], "parentKey"> & ToTreeInner<T, K>
.我们使用Omit
utility typego 掉parentKey
属性(因为我们不希望输出有parentKey
个属性).我们用递归调用ToTreeInner<T, K>
来对其进行intersect操作,这意味着我们还将在这里添加一个新树,以当前键K
为根.
这基本上就是全部内容,但如果您直接使用它并显示输出类型,您会得到一堆嵌套的Omit
、交集和DeepTreeInner
类型.因此,我使用在How can I see the full expanded contract of a Typescript type?处描述的技巧将该类型扩展为单个属性类型.我编写的是PossiblyUglyType extends infer O ? { [K in keyof O]: O[K] } : never
,而不是PossiblyUglyType
,它将PossiblyUglyType
"复制"到一个新的类型参数O
中,然后遍历O
的属性,而不修改它们.这基本上是一个大的无操作或多或少,但它影响了显示.
所以让我们试一试吧:
type Tree = ToTree<Flat>;
/* type Tree = {
key1: { property: "value1"; };
key2: {
property: "value2";
key3: {
property: "value3";
key4: { property: "value4"; };
};
};
*/
看上go 不错.我们来试试别的吧:
type Test = ToTree<{
a: { b: string, c: number },
d: { e: boolean, parentKey: "a" },
f: { g: Date, parentKey: "a" },
h: { i: any, parentKey: "d" }
}>
/* type Test = {
a: {
b: string;
c: number;
d: {
e: boolean;
h: {
i: any;
};
};
f: {
g: Date;
};
};
} */
看起来也不错!
Playground link to code个