打字稿:在“深度”对象类型中省略了对象“级别”

时间:2019-07-31 15:30:43

标签: typescript typescript-generics

考虑到可以提供对象图“扩展”版本的API,我正在寻找一种简单的方法来定义返回的类型。

以这些定义为例

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访问处理程序的使用者必须将返回类型(例如A)转换为他们请求的类型(通过提供一个参数来指示要扩展多少级)。最好根据提供的输入参数获得返回类型。

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

有人对如何改善我目前要解决的上述问题之一有任何建议吗?

P.S。 @titian-cernicova-dragomir的荣誉  从谁的answer中得出我目前拥有的东西

已编辑

已纠正@jcalz指出的错误,并根据定义的类型添加了“动态返回类型”用例的示例。

1 个答案:

答案 0 :(得分:1)

为了使您的类型定义不那么重复,我建议使用类似的东西:

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并不能真正让您执行类型级别的算术运算,因此无法从任意数中减去一个。 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中使该函数通用,则上述内容可能会有所帮助。似乎是一个单独的问题,但您是老板!

更新:看到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