Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Returntype for find() using FindOneOptions and relations #10890

Open
18 tasks
SiebelsTim opened this issue May 14, 2024 · 0 comments
Open
18 tasks

Returntype for find() using FindOneOptions and relations #10890

SiebelsTim opened this issue May 14, 2024 · 0 comments

Comments

@SiebelsTim
Copy link

Feature Description

When using find() (and friends) with relations, I'm always frustrated that the returned type does not have the resolved relations.

Consider the following Entities (decorators omitted for brevity)

class User {
  id!: number;
  group?: Group;
  groupId!: number;
  createdBy?: User | null;
  createdById!: number | null;
}

class Group {
  id!: number;
  members?: User[];
}

When using the repositories' find(options?: FindManyOptions<Entity>): Promise<Entity[]>; with passed relations, the typing does not know that these properties exist now.

async function getUser(repo: Repository<User>) {
  const users = await repo.find({ relations: { group: true } });
  const user = users[0];
  user.group; // might be undefined according to type
}

The Solution

In our project, I created the following solution:

  • First, we need to ensure that we have the relations information (e.g. {group: true}) as a type by defining it as a template (TRelations)
  • Transform that type using the Entity and Relations and remove undefined from these properties

The following example showcases the typings in a current typeorm project:

class UserService {
  constructor(private readonly repository: Repository<User>) {}

  async getUser<TRelations extends FindOptionsRelations<User>>(
    relations: TRelations,
  ): Promise<ResolveRelations<TRelations, User>[]> {
    const users = await this.repository.find({ relations });
    return users as ResolveRelations<TRelations, User>[];
  }
}

The type ResolveRelations works in the following way:

// This works in the following way:
// 1. The result is `TEntity & TResolveResult`, ie the Entity is always present
// 2. TResolveResult contains all keys of the TRelations objects 
// 3. For each Property in TRelations, remove undefined
// 4. If the property is an object, recursively call ResolveRelations
export type ResolveRelations<
  TRelations extends FindOptionsRelations<TEntity>,
  TEntity,
> = null extends TEntity
  ? (TEntity & ResolveRelationsImpl<TRelations, TEntity>) | null
  : TEntity & ResolveRelationsImpl<TRelations, TEntity>;

// Helper to allow indexing an object even if it is null
type NullableIndex<TObject, TProp extends keyof NonNullable<TObject>> = null extends TObject
  ? NonNullable<TObject>[TProp] | null
  : NonNullable<TObject>[TProp];

// The right side of `TEntity & TResolveResult` 
type ResolveRelationsImpl<TRelations extends FindOptionsRelations<TEntity>, TEntity> = {
  [P in keyof TRelations]: P extends keyof NonNullable<TEntity>
    ? TRelations[P] extends object
      ? ResolveRelations<TRelations[P], Exclude<NullableIndex<TEntity, P>, undefined>>
      : Exclude<NullableIndex<TEntity, P>, undefined>
    : never;
};

Note that I did not include the case of passing false to the relation and it probably needs a lot of work to make it work with legacy options.

Considered Alternatives

We could also use different models and interfaces for our User. One interface with populated groups, one without, etc. A major downside is that the type system does not guarantee that there wasn't any typo. Using the above approach, I get typescript errors if I mistakenly tried to populated createdByUser instead of deletedByUser.

Additional Context

I think drivers are irrelevant as it is affecting typescript only.

Relevant Database Driver(s)

  • aurora-mysql
  • aurora-postgres
  • better-sqlite3
  • cockroachdb
  • cordova
  • expo
  • mongodb
  • mysql
  • nativescript
  • oracle
  • postgres
  • react-native
  • sap
  • spanner
  • sqlite
  • sqlite-abstract
  • sqljs
  • sqlserver

Are you willing to resolve this issue by submitting a Pull Request?

No, I don’t have the time and I’m okay to wait for the community / maintainers to resolve this issue.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

1 participant