0

Given an API that can provide "expanded" versions of an object graph, I'm looking to get a simple way of defining the types returned.

As an example, for the these definitions

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;
}

I would need to derive e.g. the following types

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},
    }>,
  }>,
};

What I have are the following types (and so on for larger levels)

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;

This works fine, but there are of course two problems with this:

  1. I have to define a type for each level. It would be nice to have just two types (I assume one doesn't work or would get very messy): a leaf (level0) and a recursively defined higher order level.
  2. the consumer of my API access handler has to cast the return type (e.g. A) to the type they requested (by providing a parameter indicating how many levels to expand). It would be nice to have the return type based on the input parameter provided.

As an example for (2), say we have a function that calls backend API, e.g. getAs(levels: number): A[]. I'd like this to be getAs(levels: number): Array<RetainLevels<A, levels>> (or similar). If it isn't, the consumer has to call the method with e.g. getAs(5) as RetainLevels<A, 5>.

Does anyone have any suggestions of how to improve what I currently have to solve (one of) the above problems?

P.S. Kudos to @titian-cernicova-dragomir from who's answer I derived what I currently have

Edited

Corrected mistake as pointed out by @jcalz and added an example for "dynamic return type" use case based on the type defined.

Chris
  • 312
  • 1
  • 11

1 Answers1

2

To make your type definitions less repetitive I'd suggest something like this:

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>;

This allows you to specify types like RetainLevel<10, XXX> instead of Retain10Level<XXX>. TypeScript doesn't really let you do type-level arithmetic, so there's no way to subtract one from an arbitrary number. The PrevNum tuple is probably good enough, though; you can make it as long as whatever you deem the largest reasonable value people will ask for.


Testing to see if this gives the types you've specified:

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

These look okay.

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

Looks like your A2 type isn't the same as RetainLevel<2, A>. Although I checked that your Retain2Levels<A> is the same as RetainLevel<2, A>, so I guess your code doesn't quite constitute a minimum reproducible example. I'm going to assume it's okay.


As for the casting part, please put an example of what you mean in your code, and then maybe I or someone else will be able to answer that. If you mean that the number is passed in as a parameter, the above might help if you make the function generic in N, the type of that number. Seems like a separate question, maybe, but you're the boss!

UPDATE: seeing your example for getAs(), yes, now that RetainLevel is generic in the number N, you can make getAs() generic also:

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

This should work the way you want:

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>>;

Okay, hope that helps; good luck!

Link to code

jcalz
  • 264,269
  • 27
  • 359
  • 360
  • Thanks @jcalz for your great example! The use of array indexes to "simulate" decreasing numbers is clever ;-) I've amended the example (A2 was indeed wrong, stupid copy / paste error) and added an example for the method invocation part as requested. I don't assume there will be anything better, but I'll hold off accepting your answer for a short while. – Chris Jul 31 '19 at 19:41
  • Great, thanks! I still don't quite seem to have wrapped my head around primitives as types, could have gotten that second part myself... – Chris Aug 01 '19 at 07:43
  • One thing, though: I believe now your `Rec` does not deal with arrays of primitives correctly (there wasn't anything in my example, so my bad). I've changed it, replacing `{ id: string }` with `T extends Primitive ? T : { id: string }` and all seems fine. Thanks again! – Chris Aug 01 '19 at 07:46