打字稿:在 "Deep" 对象类型中省略对象 "Levels"

Typescript: Omitting Object "Levels" in "Deep" Object Types

鉴于 API 可以提供对象图的 "expanded" 版本,我希望获得一种定义类型的简单方法 returned.

例如,对于这些定义

interface A {
  id: string;
  q: string;
  b: B;
  cs: C[];
}

interface B {
  id: string;
  prev: B;
  c: C;
}

interface C {
  id: string;
  foo: string;
  ds: D[];
}

interface D {
  id: string;
  e: E;
}

interface E {
  id: string;
}

我需要得出例如以下类型

type A0 = {
  id: string,
  q: string,
  b: { id: string },
  cs: Array<{id: string}>,
};

type A1 = {
  id: string,
  q: string,
  b: {
    id: string,
    prev: { id: string },
    c: { id: string },
  },
  cs: Array<{
    id: string,
    foo: string,
    ds: Array<{id: string}>,
  }>,
};

type A2 = {
  id: string,
  q: string,
  b: {
    id: string,
    prev: {
      id: string,
      prev: { id: string },
      c: { id: string },
    },
    c: {
      id: string,
      foo: string,
      ds: Array<{
        id: string,
      }>,
    },
  },
  cs: Array<{
    id: string,
    foo: string,
    ds: Array<{
      id: string,
      e: { id: string},
    }>,
  }>,
};

我拥有的是以下类型(以此类推更大的级别)

export type Retain0Levels<T> = {
  [P in keyof T]:
    T[P] extends infer TP
    ? TP extends Primitive
      ? TP
      : TP extends any[]
        ? Array<{ id: string }>
        : { id: string }
   : never
};

export type Retain1Level<T> = {
  [P in keyof T]:
    T[P] extends infer TP
    ? TP extends Primitive
      ? TP
      : TP extends any[]
        ? { [I in keyof TP]: Retain0Levels<TP[I]> }
        : Retain0Levels<TP>
    : never
};

export type Retain2Levels<T> = {
  [P in keyof T]:
    T[P] extends infer TP
    ? TP extends Primitive
      ? TP
      : TP extends any[]
        ? { [I in keyof TP]: Retain1Level<TP[I]> }
        : Retain1Level<TP>
    : never
};

type Primitive = string | Function | number | boolean | symbol | undefined | null;

这很好用,但当然有两个问题:

  1. 我必须为每个级别定义一个类型。如果只有两种类型(我假设一种不起作用或会变得非常混乱)会很好:叶子(level0)和递归定义的更高阶级别。
  2. 我的 API 访问处理程序的使用者必须将 return 类型(例如 A)转换为他们请求的类型(通过提供一个参数来指示要扩展的级别).根据提供的输入参数设置 return 类型会很好。

作为 (2) 的示例,假设我们有一个调用后端 API 的函数,例如getAs(levels: number): A[]。我希望这是 getAs(levels: number): Array<RetainLevels<A, levels>> (或类似的)。如果不是,消费者必须调用该方法,例如getAs(5) as RetainLevels<A, 5>.

有没有人对如何改进我目前必须解决的(其中一个)上述问题有任何建议?

P.S。感谢 @titian-cernicova-dragomir 从谁的 我推导出我目前拥有的

已编辑

更正了@jcalz 指出的错误,并根据定义的类型为 "dynamic return type" 用例添加了示例。

为了减少类型定义的重复性,我建议这样:

type PrevNum = [-1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17];
// lengthen as needed

export type RetainLevel<N extends number, T> = {
  [P in keyof T]: T[P] extends infer TP
    ? TP extends Primitive
      ? TP
      : TP extends any[] ? { [I in keyof TP]: Rec<N, TP[I]> } : Rec<N, TP>
    : never
};

type Rec<N extends number, T> = N extends 0
  ? { id: string }
  : RetainLevel<PrevNum[N], T>;

这允许您指定类型,例如 RetainLevel<10, XXX> 而不是 Retain10Level<XXX>。 TypeScript 并不能真正让您进行类型级算术运算,因此无法从任意数字中减去 1。不过 PrevNum 元组可能已经足够好了;只要你认为人们会要求的最大合理价值,你就可以做到。


正在测试这是否给出了您指定的类型:

type MutuallyExtends<T extends U, U extends V, V = T> = true;

type A0 = RetainLevel<0, A>;
type A0Okay = MutuallyExtends<A0, A0Manual>; // okay

type A1 = RetainLevel<1, A>;
type A1Okay = MutuallyExtends<A1, A1Manual>; // okay

这些看起来不错。

type A2 = RetainLevel<2, A>;
type A2Okay = MutuallyExtends<A2, A2Manual>; // error?

您的 A2 类型似乎与 RetainLevel<2, A> 不同。虽然我检查过你的 Retain2Levels<A>RetainLevel<2, A> 相同,所以我猜你的代码并不完全构成 minimum reproducible example。我假设没关系。


关于铸造部分,请在代码中举例说明你的意思,然后也许我或其他人能够回答这个问题。如果您的意思是将数字作为参数传入,那么如果您在 N 中使函数通用,则上述内容可能会有所帮助,即该数字的类型。似乎是一个单独的问题,也许吧,但你是老板!

UPDATE:看到你的例子 getAs(),是的,现在 RetainLevel 在数字 N 中是通用的,你可以getAs() 也通用:

declare function getAs<N extends number>(levels: N): Array<RetainLevel<N, A>>;

这应该按照您想要的方式工作:

const a0s = getAs(0); // Array<RetainLevel<0, A>>, same as Array<A0>
const a1s = getAs(1); // Array<RetainLevel<1, A>>, same as Array<A1>
const a10s = getAs(10); // Array<RetainLevel<10, A>>;

好的,希望对您有所帮助;祝你好运!

Link to code