Roboroads
Roboroads

Reputation: 1709

Typescript related field building

I am trying to create an ORM generator. Currently I'm trying to simplify the way of selecting fields to return and selecting their relationships. I'm aware this can be done with just simple primitives, but it would be cool - since I already know the possible values - to have them error when a field does not exist.

I now have this:

type UserFieldSelector = '*' | 'id' | 'name' | `posts.${PostFieldSelector}`;
type PostFieldSelector = '*' | 'id' | 'title' | 'body' | `author.${UserFieldSelector}` | `comments`;
type CommentFieldSelector = '*' | 'id' | 'message' | `author.${UserFieldSelector}`;

class FieldSelection<
  FieldSelectorType extends string
  > {
  protected fields: FieldSelectorType[] = [];
  public select(...fields: FieldSelectorType[]) {
    this.fields.push(...fields);
  }
}

let x = new FieldSelection<UserFieldSelector>();
x.select("id", "name", "posts.title", "posts.author.name");

You can see TS will error here (and autocomplete doesn't work, ofcourse) because the types circularly reference each other, so unfortunately this solution is not going to work.

I was thinking about a way to just call the select function once for each field to select, and then back-reference to the argument before, but I have no idea if that's possible. Something like this (failing) code:

type UserFieldSelector = {
  '*': '*';
  'id': 'id';
  'name': 'name';
  'posts': PostFieldSelector;
};
type PostFieldSelector = {
  '*': '*';
  'id': 'id';
  'title': 'title';
  'body': 'body';
  'author': UserFieldSelector;
}
type CommentFieldSelector = {
  '*': '*';
  'id': 'id';
  'message': 'message';
  'author': UserFieldSelector;
}

class FieldSelection<
  FieldSelectorType extends { [k: string]: unknown }
  > {
  protected fields: FieldSelectorType[] = [];
  public select(field: keyof FieldSelectorType, ...related: keyof FieldSelectorType[field]): this {
    // Something to figure out
    return this;
  }
}

let x = new FieldSelection<UserFieldSelector>();
x.select("id")
  .select("posts", "title")
  .select("posts", "author", "name");

I was hoping you have an other idea about how to solve this. I've been botching and bickering for days now and am out of ideas.

Upvotes: 0

Views: 28

Answers (1)

DemiPixel
DemiPixel

Reputation: 1881

If you're okay with this format, it will auto-complete and error with invalid properties.

public select(depth: Depth<FieldSelectorType>): this {}

// ...

let x = new FieldSelection<UserFieldSelector>();
x.select({
  id: {},
  posts: {
    author: { name: {} }
  }
})

with the object types you specified above, along with Depth defined as:

type FlattenIfArr<T> = T extends (infer R)[] ? R : T;

export type Depth<T> = Partial<
  { [K in keyof T]: NonNullable<T[K]> extends object ?
    Depth<NonNullable<FlattenIfArr<T[K]>>> : {} }
>;

Note, this is built specifically for the original class names/types, hence why I added NonNullable and FlattenIfArr, since you probably would have something like posts?: Post[].

This solution is based off this specific section of my blog post which achieves something similar using TypeORM.

Upvotes: 1

Related Questions