给定以下对象 struct :

type IObject = { id: string, path: string, children?: IObject[] }

const tree = [
    {
      id: 'obj1' as const,
      path: 'path1' as const,
      children: [
        {
          id: 'obj2' as const,
          path: ':pathParam' as const,
          children: [
            {
              id: 'obj3' as const,
              path: 'path3' as const
            }
          ]
        },
        {
          id: 'obj4' as const,
          path: 'path4' as const,
          children: [
            {
              id: 'obj5' as const,
              path: 'path5' as const
            }
          ]
        }
      ]
    }
  ];

我正在try 提取嵌套对象的类型,并将path个片段累加到该嵌套类型.(假设ID在整个对象中是唯一的) 示例

type ok = FindAndTrackDeepPath<'obj2', typeof tree>['fullPath'];
// type ok = "/path1/:pathParam"

我try 了以下我认为很接近的方法.然而,正如您在第二个(非工作的)示例中所看到的,递归地向下递归该类型时似乎存在一个问题.

export type FindAndTrackDeepPath<
  ID,
  T extends IObject[],
  depth extends number = 1,
  path extends string = '',
  maxDepth extends number = 10
> = depth extends maxDepth
  ? never
  : FindByID<ID, T> extends never
    ? FindAndTrackDeepPath<
        ID,
        Descend<T>['children'],
        Increment<depth>,
        Descend<T>['path'] extends undefined ? '' : `${path}/${Descend<T>['path']}`
      >
    : FindByID<ID, T> & { fullPath: `${path}/${FindByID<ID, T>['path']}` };


// helpers
type FindByID<ID, T extends IObject[]> = Extract<T[number], { id: ID }>;
type Descend<T extends IObject[]> = Extract<T[number], { children: IObject[] }>;
type Increment<N extends number> = [1,2,3,4,5,6,7,8,9,10,11,12,...number[]][N];

// examples
type ok = FindAndTrackDeepPath<'obj2', typeof tree>['fullPath'];
// type ok = "/path1/:pathParam"
type notOK = FindAndTrackDeepPath<'obj3', typeof tree>['fullPath'];
// type notOK = "/path1/:pathParam/path3" | "/path1/path4/path3"

type notOK有两种 Select ,其中一种显然是错误的.

我认为问题在于,当递归下降时,每一层的所有选项的联合是以一种"广度优先"的方式创建的.(例如,发生这样的事情:${("/path 1/:pathParam")|"/path1/path4")}/path3"}) 我已经try 了几种方法来进一步修剪不成功的下降/"unions 的一部分",而下降.但毫无结果.

我找到了几个相关的问题herehere.然而,这些方法提取了所有可能的路径.相反,我只想提取与特定条件(在我的例子中为extends { id: ID})匹配的子类型的路径.

有谁能帮帮我吗?这有可能吗?

TS Playground example

推荐答案

下面是我所说的IdxWithFullPath<I, T>的一个可能的实现,其中I是您在IObjectIObject[] T类型中寻找的generic id类型.它将求值为IObject中可分配给类型Iidintersection,以及一个具有fullPath属性的对象,该属性对应于树中从根到所述IObject的所有path个属性的串联:

type IdxWithFullPath<I, T, P extends string = ""> =
  T extends IObject[] ? IdxWithFullPath<I, T[number], P> :
  T extends IObject ? (
    I extends T["id"] ? (
      T & { fullPath: `${P}/${T["path"]}` }
    ) : (
      IdxWithFullPath<I, T["children"], `${P}/${T["path"]}`>
    )
  ) :
  never;

还有第三个泛型类型参数P对应于父路径;这允许IdxWithFullPath是递归的.它将defaults赋给空字符串文字类型,即树根的路径.

请注意,它是T中的distributive conditional type.这意味着如果Tunion,那么它将被拆分成单独的成员,这些成员将被单独处理,结果将被重新连接到一个联盟中.由于the never type被吸收到unions 中,这意味着我们可以为任何我们不想包括的内容返回never.

因此,如果T是一个由IObject组成的数组,那么我们想要对T[number]进行运算,T[number]IObject个元素类型的并集.

否则,如果TIObject,我们判断它的id属性.如果它与I匹配,则返回所需的交叉点,其中fullPath是存储在P中的父路径,与当前IObjectpath属性串联,使用template literal types.如果它doesn'tI匹配,那么我们必须向下递归到children,将path附加到P作为新的父路径.

最后,如果T是其他值,那么我们返回never,这样它就不会包含在结果中.这将在搜索触底时发生,例如缺少children属性:我认为如果缺少children,则T["children"]的计算结果为unknown,如果children存在但为undefined,则T["children"]的计算结果为undefined.我们希望这两个都不包括在内.


让我们来测试一下:

type Ok = IdxWithFullPath<'obj2', typeof tree>;
/* type Ok = {
    id: "obj2";
    path: ":pathParam";
    children: {
        id: "obj3";
        path: "path3";
    }[];
} & {
    fullPath: "/path1/:pathParam";
} */

type AlsoOK = IdxWithFullPath<'obj3', typeof tree>;
/* type AlsoOK = {
    id: "obj3";
    path: "path3";
} & {
    fullPath: "/path1/:pathParam/path3";
} */

看上go 不错.请注意,这种深度递归的条件类型往往具有奇怪的边缘情况,有时需要进行重大重构才能解决.对于任何这样的类型,您都应该在继续之前针对预期的用例进行大量测试,并准备好在发现不可接受的糟糕边缘情况行为时修改甚至放弃方法.

Playground link to code

Typescript相关问答推荐

创建一个将任意命名类型映射到其他类型的TypScript通用类型

Typescript如何从字符串中提取多个文本

从接口创建类型安全的嵌套 struct

TypeScript实用程序类型—至少需要一个属性的接口,某些指定属性除外

为什么typescript要把这个空合并转换成三元?

如何访问Content UI的DatePicker/中的sx props of year picker?'<>

带下拉菜单的Angular 响应式选项卡

在Typescript 中,有没有`index=unfined的速记?未定义:某个数组[索引]`?

Angular 17子路由

如何使所有可选字段在打印脚本中都是必填字段,但不能多或少?

在排版修饰器中推断方法响应类型

如何编写在返回函数上分配了属性的类型安全泛型闭包

有没有一种方法可以确保类型的成员实际存在于TypeScript中的对象中?

在Mac和Windows上运行的Web应用程序出现这种对齐差异的原因是什么?(ReactNative)

如何在Reaction Native中关注屏幕时正确更新状态变量?

类型';只读<;部分<;字符串|记录<;字符串、字符串|未定义&>';上不存在TS 2339错误属性.属性&q;x&q;不存在字符串

如何向我的自定义样式组件声明 custom.d.ts ?

Typescript 子类继承的方法允许参数中未定义的键

为什么 Typescript 无法正确推断数组元素的类型?

在Typescript 中,是否有一种方法来定义一个类型,其中该类型是字符串子集的所有可能组合?