2
votes

When constraining a generic type for a class, ts complains that the generic could be instantiated with a different subtype. Anyone have any idea what that actually means (and what to do about it)?

Reproduction (TypeScript 3.6.4):

interface BaseSchema {
  readonly id: string;
}

class Model<GenericSchema extends BaseSchema> {
  public test(): GenericSchema {
    return { id: '' };
  }
}

Error:

Type '{ id: string; }' is not assignable to type 'GenericSchema'. '{ id: string; }' is assignable to the constraint of type 'GenericSchema', but 'GenericSchema' could be instantiated with a different subtype of constraint 'BaseSchema'.ts(2322)

Seems to be related to https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-6.html#strict-function-types

I can make it work with a type assertion, but it does not seem right:

interface BaseSchema {
  readonly id: string;
}

export class Model<GenericSchema extends BaseSchema> {
  public test2(): GenericSchema {
    return { id: '' } as GenericSchema;
  }
}

UPDATE: New example below. In the example we provide a generic MySchema when instantiating Model, and this expect the test() method to return MySchema.

class Model<
  GenericSchema extends {
    readonly id: string;
  }
> {
  public test() {
    const dbResult: GenericSchema = { id: '' };
    return dbResult;
  }
}

interface MySchema {
  readonly id: string;
  readonly name: string;
}

const model = new Model<MySchema>();
const schema: MySchema = model.test();

These are all contrived examples, the real use case is to access a DB and return data for which I have type annotations. Essentially MySchema would represent the data model in the database, so I want to provide it as a generic (since TS can't possibly know what the data looks like in the DB otherwise).

2

2 Answers

2
votes

Maybe tweaking some of the names will help with the concept of generics. You should always prefix generic type identifiers with T to make it obvious what they are. (In this case I chose TSchema, but you could also use just T)

The reason it gives an error is because TSchema can be ANYTHING in the universe that extends BaseSchema. And { id: '' } absolutely does not fulfill "everything".

interface BaseSchema {
  id: string;
}

class Model<TSchema extends BaseSchema> {
  public test() {
    const dbResult: TSchema = { id: '' };
    return dbResult;
  }
}

You can't just create an object with an ID and claim it is of type T, as you can create any interface/class you want that extends BaseSchema. The only assumption you can make is that it extends BaseSchema.


If you just want a way to access data and return it with the correct type, then the result from the service (data access or HTTP or anything else) should return either a typed value or any value that can be casted. If that is the case there is nothing wrong with direct casting as you can assume it came back from the database in the right format (just make sure you validate models before saving to the database!).

Simple Example:

interface BaseSchema {
  readonly id: string;
}

class Model<T extends BaseSchema> {

  private svc: SomeService;

  public constructor(private table: string) {
  }

  public get(id: string): T {
    return this.svc.getItemFromTableWithId(this.table, id) as T;
  }

  public getAll(): T[] {
    return this.svc.getAllFromTable(this.table) as T[];
  }

}

interface BlogArticle extends BaseSchema {
  name: string;
  author: string;
  lastUpdated: Date;
  published: boolean;
}

const blogArticles = new Model<BlogArticle>('blog-articles');

blogArticles.get(4); // Returns an instance of `BlogArticle`.
blogArticles.getAll(); // Returns an instance of `BlogArticle[]`.

1
votes

Consider the following scenario:

interface BaseSchema {
  readonly id: string;
}

class Model<GenericSchema extends BaseSchema> {
  public test(): GenericSchema {
    return { id: '' };
  }
}

interface MySchema extends BaseSchema {
   readonly name: string;
}

var model = new Model<MySchema>();
var schema = model.test();

Does test return a MySchema? Well there's no name property, so no, it returns BaseSchema. Essentially that is what the error is trying to tell you in a somewhat roundabout way. The return type of test should be BaseSchema since the only property it has is id and you can't guarantee you'll return any particular subtype of it.

Type '{ id: string; }' is not assignable to type 'GenericSchema'.

True, GenericSchema could have any number of properties, we have no idea of its shape.

'{ id: string; }' is assignable to the constraint of type 'GenericSchema'

In other words { id: string; } can be assigned to BaseSchema.

but 'GenericSchema' could be instantiated with a different subtype of constraint 'BaseSchema'

GenericSchema could be anything that extends BaseSchema, how do we guarantee we're returning a type that isn't known to us?