我正在try 正确地键入一个函数的返回值,该函数将树状 struct 的平面配置转换为树状 struct 本身,例如:

把这件事

{
  key1: { property: "value1" },
  key2: { property: "value2" },
  key3: { property: "value3", parentKey: "key2"},
  key4: { property: "value4", parentKey: "key3" }
}

vt.进入,进入

{
  key1: { property: "value1", ... },
  key2: {
    property: "value2",
    key3: {
      property: "value3", ...,
      key4: { property: "value4", ... }
    }
  }
}

以一种捕获键名称的方式,而不仅仅是允许嵌套的字符串索引(可能不存在键).

我试图从输入配置(上面的第一个代码片断)开始,但没有太多运气,以使TypeScrip正确地推断它们的键联合类型,同时将"parentKey"限制为可用值,例如:

type Config<T extends string> = Record<T, Properties & { parentKey?: T }>;

不起作用,因为TypeScrip将T缩小为在parentKey中指定的值的联合.不将parentKey绑定到其他关键字(例如,将其指定为string而不是T)会使TS将T推断为所有关键字的并集,但允许将不存在的值作为父项.


Reason behind trying to do something like this is to embed some default property values/behaviors vt.进入,进入 each node of the tree without need in specifying them manually repeatedly.


感谢任何关于这是否可能表达的建议,如果可能,那么如何接近它.此外,也许有一种更好的形式来表示配置和生成的树,它可以在允许更好的推理的同时仍然允许对每个 node 进行一些扩充-如果有任何其他 idea 将是很好的.

推荐答案

我只考虑类型级别的转换,而不考虑类型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 }
) }>;

这允许Toptionally的每个属性都有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,它是defaultsundefined,所以您可以省略那个类型参数来获得完整的树.

然后我们有一个key-remapped type { [K in keyof T as P extends T[K]["parentKey"] ? K : never]: ⋯ },它使用as子句来筛选键.如果P extends T[K]["parentKey"],则输出对象将仅具有键K,这意味着如果T中的相应属性具有parentKeyP,则输出对象将仅具有键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

Typescript相关问答推荐

动态判断TypScript对象是否具有不带自定义类型保护的键

泛型和数组:为什么我最终使用了`泛型T []`而不是`泛型T []`?<><>

创建一个根据布尔参数返回类型的异步函数

如何在Typescript 中输入两个相互关联的变量?

如何在另一个参数类型中获取一个参数字段的类型?

通配符路径与每条路径一起显示

类型脚本返回的类型包含无意义的泛型类型名称

`fetcher.load`是否等同于Get Submit?

以Angular 执行其他组件的函数

下载量怎么会超过下载量?

如何过滤文字中的类对象联合类型?

Angular NG8001错误.组件只能在一个页面上工作,而不是在她的页面上工作

如何在本地存储中声明该类型?

埃斯林特警告危险使用&as";

推断集合访问器类型

使用&reaction测试库&如何使用提供程序包装我的所有测试组件

如何在TypeScrip中使用数组作为字符串的封闭列表?

什么';是并类型A|B的点,其中B是A的子类?

传入类型参数<;T>;变容函数

Typescript 和 Rust 中跨平台的位移数字不一致