Search code examples
node.jstypescriptnestjstypeorm

Why Generics does not extends DeepPartial?


I am using the clean architecture for the NestJS from this GitHub repository https://github.com/hvpaiva/clean-architecture-nestjs.

After updating the packages, I see an issue with the BaseRepository, especially with this and similar parts (save, softRemove, )

save<T extends DeepPartial<Entity>>(
  entityOrEntities: T | T[],
  options?: SaveOptions,
): Promise<T | T[]> {
  return this.manager.save<T>(
    this.entitySchema as any,
    entityOrEntities as any,
    options,
  );
}

The error says

 TS2322: Type 'Promise<DeepPartial<Entity>[]>' is not assignable to type 'Promise<T | T[]>'.
  Type 'DeepPartial<Entity>[]' is not assignable to type 'T | T[]'.
    Type 'DeepPartial<Entity>[]' is not assignable to type 'T[]'.
      Type 'DeepPartial<Entity>' is not assignable to type 'T'.
        'DeepPartial<Entity>' is assignable to the constraint of type 'T', but 'T' could be instantiated with a different subtype of constraint 'ObjectLiteral | { [x: string]: any; }'.
          Type 'Entity' is not assignable to type 'T'.
            'Entity' is assignable to the constraint of type 'T', but 'T' could be instantiated with a different subtype of constraint 'ObjectLiteral | { [x: string]: any; }'.

Packages are:

"dependencies": {
  "@godaddy/terminus": "^4.11.2",
  "@nestjs/common": "^9.1.6",
  "@nestjs/config": "^2.2.0",
  "@nestjs/core": "^9.1.6",
  "@nestjs/platform-express": "^9.1.6",
  "@nestjs/swagger": "^6.1.3",
  "@nestjs/terminus": "^9.1.2",
  "@nestjs/typeorm": "^9.0.1",
  "body-parser": "^1.20.1",
  "cache-manager": "^5.1.1",
  "chalk": "^5.1.2",
  "class-transformer": "^0.5.1",
  "class-validator": "^0.13.2",
  "compression": "^1.7.4",
  "cross-env": "^7.0.3",
  "express-rate-limit": "^6.6.0",
  "helmet": "^6.0.0",
  "pg": "^8.8.0",
  "reflect-metadata": "^0.1.13",
  "rimraf": "^3.0.2",
  "rxjs": "^7.5.7",
  "swagger-ui-express": "^4.5.0",
  "typeorm": "^0.3.10"
},
"devDependencies": {
  "@nestjs/cli": "^9.1.5",
  "@nestjs/schematics": "^9.0.3",
  "@nestjs/testing": "^9.1.6",
  "@types/compression": "^1.7.2",
  "@types/express": "^4.17.14",
  "@types/express-rate-limit": "^5.1.3",
  "@types/helmet": "^0.0.48",
  "@types/jest": "29.2.0",
  "@types/lodash": "^4.14.186",
  "@types/node": "^18.11.8",
  "@types/supertest": "^2.0.12",
  "@typescript-eslint/eslint-plugin": "^5.41.0",
  "@typescript-eslint/parser": "^5.41.0",
  "eslint": "^8.26.0",
  "eslint-config-prettier": "^8.5.0",
  "eslint-plugin-import": "^2.26.0",
  "eslint-plugin-import-helpers": "^1.3.1",
  "jest": "^29.2.2",
  "lodash": "^4.17.21",
  "prettier": "^2.7.1",
  "start-server-webpack-plugin": "^2.2.5",
  "supertest": "^6.3.1",
  "ts-jest": "29.0.3",
  "ts-loader": "^9.4.1",
  "ts-node": "^10.9.1",
  "tsconfig-paths": "^4.1.0",
  "typescript": "^4.8.4",
  "webpack-node-externals": "^3.0.0"
}

Full BaseRepository.ts file

import {
  ObjectLiteral,
  EntityManager,
  QueryRunner,
  DeepPartial,
  SaveOptions,
  RemoveOptions,
  InsertResult,
  ObjectID,
  FindConditions,
  UpdateResult,
  DeleteResult,
  FindManyOptions,
  FindOneOptions,
  EntitySchema,
  Connection,
} from 'typeorm';
import { QueryDeepPartialEntity } from 'typeorm/query-builder/QueryPartialEntity';

export class BaseRepository<Entity extends ObjectLiteral> {
  readonly manager: EntityManager;
  readonly queryRunner?: QueryRunner;
  readonly entitySchema: EntitySchema<Entity>;

  constructor(connection: Connection, entity: EntitySchema<Entity>) {
    this.queryRunner = connection.createQueryRunner();
    this.manager = this.queryRunner.manager;
    this.entitySchema = entity;
  }

  hasId(entity: Entity): boolean {
    return this.manager.hasId(entity);
  }

  getId(entity: Entity): any {
    return this.manager.getId(entity);
  }

  create(): Entity;

  create(entityLikeArray: DeepPartial<Entity>[]): Entity[];

  create(entityLike: DeepPartial<Entity>): Entity;

  create(
    plainEntityLikeOrPlainEntityLikes?:
      | DeepPartial<Entity>
      | DeepPartial<Entity>[],
  ): Entity | Entity[] {
    return this.manager.create<any>(
      this.entitySchema as any,
      plainEntityLikeOrPlainEntityLikes as any,
    );
  }

  merge(
    mergeIntoEntity: Entity,
    ...entityLikes: DeepPartial<Entity>[]
  ): Entity {
    return this.manager.merge(
      this.entitySchema as any,
      mergeIntoEntity,
      ...entityLikes,
    );
  }

  preload(entityLike: DeepPartial<Entity>): Promise<Entity | undefined> {
    return this.manager.preload(this.entitySchema as any, entityLike);
  }

  save<T extends DeepPartial<Entity>>(
    entities: T[],
    options: SaveOptions & { reload: false },
  ): Promise<T[]>;

  save<T extends DeepPartial<Entity>>(
    entities: T[],
    options?: SaveOptions,
  ): Promise<(T & Entity)[]>;

  save<T extends DeepPartial<Entity>>(
    entity: T,
    options: SaveOptions & { reload: false },
  ): Promise<T>;

  save<T extends DeepPartial<Entity>>(
    entity: T,
    options?: SaveOptions,
  ): Promise<T & Entity>;

  save<T extends DeepPartial<Entity>>(
    entityOrEntities: T | T[],
    options?: SaveOptions,
  ): Promise<T | T[]> {
    return this.manager.save<T>(
      this.entitySchema as any,
      entityOrEntities as any,
      options,
    );
  }

  remove(entities: Entity[], options?: RemoveOptions): Promise<Entity[]>;

  remove(entity: Entity, options?: RemoveOptions): Promise<Entity>;

  remove(
    entityOrEntities: Entity | Entity[],
    options?: RemoveOptions,
  ): Promise<Entity | Entity[]> {
    return this.manager.remove(
      this.entitySchema as any,
      entityOrEntities as any,
      options,
    );
  }

  softRemove<T extends DeepPartial<Entity>>(
    entities: T[],
    options: SaveOptions & { reload: false },
  ): Promise<T[]>;

  softRemove<T extends DeepPartial<Entity>>(
    entities: T[],
    options?: SaveOptions,
  ): Promise<(T & Entity)[]>;

  softRemove<T extends DeepPartial<Entity>>(
    entity: T,
    options: SaveOptions & { reload: false },
  ): Promise<T>;

  softRemove<T extends DeepPartial<Entity>>(
    entity: T,
    options?: SaveOptions,
  ): Promise<T & Entity>;

  softRemove<T extends DeepPartial<Entity>>(
    entityOrEntities: T | T[],
    options?: SaveOptions,
  ): Promise<T | T[]> {
    return this.manager.softRemove<T>(
      this.entitySchema as any,
      entityOrEntities as any,
      options,
    );
  }

  recover<T extends DeepPartial<Entity>>(
    entities: T[],
    options: SaveOptions & { reload: false },
  ): Promise<T[]>;

  recover<T extends DeepPartial<Entity>>(
    entities: T[],
    options?: SaveOptions,
  ): Promise<(T & Entity)[]>;

  recover<T extends DeepPartial<Entity>>(
    entity: T,
    options: SaveOptions & { reload: false },
  ): Promise<T>;

  recover<T extends DeepPartial<Entity>>(
    entity: T,
    options?: SaveOptions,
  ): Promise<T & Entity>;

  recover<T extends DeepPartial<Entity>>(
    entityOrEntities: T | T[],
    options?: SaveOptions,
  ): Promise<T | T[]> {
    return this.manager.recover<T>(
      this.entitySchema as any,
      entityOrEntities as any,
      options,
    );
  }

  insert(
    entity: QueryDeepPartialEntity<Entity> | QueryDeepPartialEntity<Entity>[],
  ): Promise<InsertResult> {
    return this.manager.insert(this.entitySchema as any, entity);
  }

  update(
    criteria:
      | string
      | string[]
      | number
      | number[]
      | Date
      | Date[]
      | ObjectID
      | ObjectID[]
      | FindConditions<Entity>,
    partialEntity: QueryDeepPartialEntity<Entity>,
  ): Promise<UpdateResult> {
    return this.manager.update(
      this.entitySchema as any,
      criteria as any,
      partialEntity,
    );
  }

  delete(
    criteria:
      | string
      | string[]
      | number
      | number[]
      | Date
      | Date[]
      | ObjectID
      | ObjectID[]
      | FindConditions<Entity>,
  ): Promise<DeleteResult> {
    return this.manager.delete(this.entitySchema as any, criteria as any);
  }

  softDelete(
    criteria:
      | string
      | string[]
      | number
      | number[]
      | Date
      | Date[]
      | ObjectID
      | ObjectID[]
      | FindConditions<Entity>,
  ): Promise<UpdateResult> {
    return this.manager.softDelete(this.entitySchema as any, criteria as any);
  }

  restore(
    criteria:
      | string
      | string[]
      | number
      | number[]
      | Date
      | Date[]
      | ObjectID
      | ObjectID[]
      | FindConditions<Entity>,
  ): Promise<UpdateResult> {
    return this.manager.restore(this.entitySchema as any, criteria as any);
  }

  count(options?: FindManyOptions<Entity>): Promise<number>;

  count(conditions?: FindConditions<Entity>): Promise<number>;

  count(
    optionsOrConditions?: FindManyOptions<Entity> | FindConditions<Entity>,
  ): Promise<number> {
    return this.manager.count(
      this.entitySchema as any,
      optionsOrConditions as any,
    );
  }

  find(options?: FindManyOptions<Entity>): Promise<Entity[]>;

  find(conditions?: FindConditions<Entity>): Promise<Entity[]>;

  find(
    optionsOrConditions?: FindManyOptions<Entity> | FindConditions<Entity>,
  ): Promise<Entity[]> {
    return this.manager.find(
      this.entitySchema as any,
      optionsOrConditions as any,
    );
  }

  findAndCount(options?: FindManyOptions<Entity>): Promise<[Entity[], number]>;

  findAndCount(
    conditions?: FindConditions<Entity>,
  ): Promise<[Entity[], number]>;

  findAndCount(
    optionsOrConditions?: FindManyOptions<Entity> | FindConditions<Entity>,
  ): Promise<[Entity[], number]> {
    return this.manager.findAndCount(
      this.entitySchema as any,
      optionsOrConditions as any,
    );
  }

  findByIds(ids: any[], options?: FindManyOptions<Entity>): Promise<Entity[]>;

  findByIds(ids: any[], conditions?: FindConditions<Entity>): Promise<Entity[]>;

  findByIds(
    ids: any[],
    optionsOrConditions?: FindManyOptions<Entity> | FindConditions<Entity>,
  ): Promise<Entity[]> {
    return this.manager.findByIds(
      this.entitySchema as any,
      ids,
      optionsOrConditions as any,
    );
  }

  findOne(
    id?: string | number | Date | ObjectID,
    options?: FindOneOptions<Entity>,
  ): Promise<Entity | undefined>;

  findOne(options?: FindOneOptions<Entity>): Promise<Entity | undefined>;

  findOne(
    conditions?: FindConditions<Entity>,
    options?: FindOneOptions<Entity>,
  ): Promise<Entity | undefined>;

  findOne(
    optionsOrConditions?:
      | string
      | number
      | Date
      | ObjectID
      | FindOneOptions<Entity>
      | FindConditions<Entity>,
    maybeOptions?: FindOneOptions<Entity>,
  ): Promise<Entity | undefined> {
    return this.manager.findOne(
      this.entitySchema as any,
      optionsOrConditions as any,
      maybeOptions,
    );
  }

  findOneOrFail(
    id?: string | number | Date | ObjectID,
    options?: FindOneOptions<Entity>,
  ): Promise<Entity>;

  findOneOrFail(options?: FindOneOptions<Entity>): Promise<Entity>;

  findOneOrFail(
    conditions?: FindConditions<Entity>,
    options?: FindOneOptions<Entity>,
  ): Promise<Entity>;

  findOneOrFail(
    optionsOrConditions?:
      | string
      | number
      | Date
      | ObjectID
      | FindOneOptions<Entity>
      | FindConditions<Entity>,
    maybeOptions?: FindOneOptions<Entity>,
  ): Promise<Entity> {
    return this.manager.findOneOrFail(
      this.entitySchema as any,
      optionsOrConditions as any,
      maybeOptions,
    );
  }

  query(query: string, parameters?: any[]): Promise<any> {
    return this.manager.query(query, parameters);
  }

  clear(): Promise<void> {
    return this.manager.clear(this.entitySchema);
  }

  increment(
    conditions: FindConditions<Entity>,
    propertyPath: string,
    value: number | string,
  ): Promise<UpdateResult> {
    return this.manager.increment(
      this.entitySchema,
      conditions,
      propertyPath,
      value,
    );
  }

  decrement(
    conditions: FindConditions<Entity>,
    propertyPath: string,
    value: number | string,
  ): Promise<UpdateResult> {
    return this.manager.decrement(
      this.entitySchema,
      conditions,
      propertyPath,
      value,
    );
  }

  async transaction<T>(operation: () => Promise<T>): Promise<T> {
    await this.queryRunner.connect();
    await this.queryRunner.startTransaction();

    try {
      const result = await operation();

      await this.queryRunner.commitTransaction();
      return result;
    } catch (err) {
      await this.queryRunner.rollbackTransaction();
    } finally {
      await this.queryRunner.release();
    }
  }
}

I find this discussion, but it was not helpful: https://github.com/typeorm/typeorm/issues/8681

I did a research and tried to do changes in the DeepPartial, but it does not help. My expectation is to solve the problem which causes the error.


Solution

  • The actual problem is caused because the save method wants also Entity.

    
    save<T extends DeepPartial<Entity>>(
        entityOrEntities: T | T[],
        options?: SaveOptions,
      ): Promise<T | T[]> {
        return this.manager.save<Entity, T>(
          this.entitySchema as any,
          entityOrEntities as any,
          options,
        );
      }