DEV Community

chelproc
chelproc

Posted on

Type-Safe Relation Expression for TypeORM

TypeORM is an amazing JavaScript/TypeScript ORM library. With TypeORM, you can safely query any database to fetch whatever data you want.

However, relations option in find API, which is used when you need other entities connected by foreign constraints at the same time, is just typed as string[]. This may cause unexpected regression when you alter table column names.

Let's take a typical data model of Users and Organizations for example.

type Organization = {
  id: number;
  name: string;
  users: User[];
}

type UserRole = {
  id: number;
  type: string;
};

type User = {
  id: number;
  name: string;
  organization: Organization;
  roles: UserRole[];
};
Enter fullscreen mode Exit fullscreen mode

Assume you have User's id and want to retrieve all Users that belong to its organization with their roles, then you need to pass "organization.users.roles" to relations option.

userRepository.find({
  where: { id },
  relations: ["organization.users.roles"],
});
Enter fullscreen mode Exit fullscreen mode

TypeScript provides no support at all! 😱

This problem can be solved with TypeScript's new feature called template literal types.

type RelationInner<
  T,
  Key,
  Depth extends number,
  CurrentDepthArray extends unknown[]
> = Key extends keyof T & string
  ? T[Key] extends Function | string | number | symbol
    ? never
    :
        | Key
        | (CurrentDepthArray["length"] extends Depth
            ? never
            : `${Key}.${RelationInner<
                T[Key] extends (infer R)[] ? R : T[Key],
                keyof (T[Key] extends (infer R)[] ? R : T[Key]),
                Depth,
                [unknown, ...CurrentDepthArray]
              >}`)
  : never;
type Relation<T, Depth extends number> = RelationInner<T, keyof T, Depth, []>;
Enter fullscreen mode Exit fullscreen mode

Template literal types enables developers to concatenate or split arbitrary strings in a similar syntax to JavaScript's template literals.

Together with distributive conditional types, you can recursively join deeply buried property names with periods.

image

Don't forget to limit recursion depth! Otherwise you will end up getting errors like Type instantiation is excessively deep and possibly infinite.

Top comments (0)