Skip to content

Constructor of a generic class cannot be assigned to a generic constructor-function type because of premature specialization #31840

Closed
@bajtos

Description

@bajtos

TypeScript Version: 3.6.0-dev.20190608

Search Terms:

Code

export class Entity {
  // members are not important
}

/**
 * A generic interface.
 *
 * NOTE:
 * The first generic parameter is required,
 * the second one is optional.
 */
export interface EntityCrudRepository<
  T extends Entity,
  Relations extends object = {}
> {
  findFirst(): Promise<T & Relations>;
}

/**
 * A generic class implementing the generic interface.
 */
export class DefaultCrudRepository<
  T extends Entity,
  Relations extends object = {}
> implements EntityCrudRepository<T, Relations> {
  constructor(
    private entityClass: typeof Entity & {prototype: T},
    private dataSource: object,
  ) {}

  async findFirst(): Promise<T & Relations> {
    // dummy implementation, not important
    return {} as (T & Relations);
  }
}

/**
 * A type representing any generic class that's implementing 
 * our generic interface and providing a two-arg constructor.
 * E.g. `DefaultCrudRepository`, but also any other class
 * matching the required API contracts.
 */
export type CrudRepositoryCtor = new <
  T extends Entity,
  Relations extends object
>(
  entityClass: typeof Entity & {prototype: T},
  dataSource: object,
) => EntityCrudRepository<T, Relations>;

/**
 * A test-suite builder wants to accept any `EntityCrudRepository` 
 * implementation, so that we can define the same set of tests
 * for different repository classes.
 */
export function crudRepositoryTestSuite(
  dataSourceOptions: object,
  repositoryClass: CrudRepositoryCtor,
) {
  // write tests calling `repositoryClass` ctor to obtain
  // an instance of the repository for the given Entity and DataSource
  // (entities are defined by individual test cases)
}

/**
 * Usage in tests.
 */
//describe('DefaultCrudRepository with memory connector', () => {
  crudRepositoryTestSuite(
    {connector: 'memory'},
    DefaultCrudRepository,
  );
//});

Expected behavior:

The program compiles with no errors.

Actual behavior:

$ tsc --lib es2017 tsbug.ts
tsbug.ts:46:5 - error TS2345: Argument of type 'typeof DefaultCrudRepository' is not assignable to parameter of type 'CrudRepositoryCtor'.
  Type 'DefaultCrudRepository<Entity, {}>' is not assignable to type 'EntityCrudRepository<T, Relations>'.
    Types of property 'findFirst' are incompatible.
      Type '() => Promise<Entity>' is not assignable to type '() => Promise<T & Relations>'.
        Type 'Promise<Entity>' is not assignable to type 'Promise<T & Relations>'.
          Type 'Entity' is not assignable to type 'T & Relations'.
            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 'Entity'.

46     DefaultCrudRepository,
       ~~~~~~~~~~~~~~~~~~~~~


Found 1 error.

This part seems most relevant to me:

Type 'DefaultCrudRepository<Entity, {}>' is not assignable to type 'EntityCrudRepository<T, Relations>'

IIUC, the compiler is not able to treat DefaultCrudRepository as a generic constructor, instead if specializes the constructor function too early using "sensible" default values for generic arguments.

Playground Link:

the link

Unfortunately, I am not able to reproduce the problem in the Playground.

Related Issues:

Metadata

Metadata

Assignees

No one assigned

    Labels

    QuestionAn issue which isn't directly actionable in code

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions