Typescript: Generic constraints as return type in class methods

Created on 13 Nov 2019  路  4Comments  路  Source: microsoft/TypeScript

TypeScript Version: 3.6.4 & 3.7.2 & 3.8.0-dev.20191112

Search Terms: generic constraints, TS2322, 2322

Code

export class Model<GenericSchema extends { id: string }> {
  public fails(): GenericSchema {
    return { id: '' };  // <-- This line produce the error, see below
  }

  public works(): GenericSchema {
    return { id: '' } as GenericSchema;
  }
}

Expected behavior:
Should compile without errors. To me this should work since the the fails() method returns an object which satisfies the constraint { id: string }.

Actual behavior:

3:5 - error TS2322: 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 '{ id: string; }'.

Context:
For additional context, I originally filed an issue with @types/mongodb which gives the original use case
https://github.com/DefinitelyTyped/DefinitelyTyped/issues/39358

Last, here is a discussion of the exact same matter on Stack Overflow
https://stackoverflow.com/questions/58663733/typescript-class-generic-constraint

Playground Link:
https://codesandbox.io/s/mapped-type-with-generics-l4b0b

Related Issues:
I believe this is related to https://github.com/microsoft/TypeScript/issues/34567 and as such probably also https://github.com/microsoft/TypeScript/issues/33014.

Working as Intended

Most helpful comment

All 4 comments

The error here is correct. Model could be instantiated, for example, with GenericSchema = { id: string, foo: string }, which is assignable to { id: string }. The inverse, however, is not true.

To me this should work since the the fails() method returns an object which satisfies the constraint { id: string }.

It satisfies the constraint, yes, but it doesn't satisfy all possible types that GenericSchema could be. Type parameters are universally quantified--that's the purpose of using a generic.

Here's a concrete example of the kind of error TS is trying to protect you from,

export class Model<GenericSchema extends { id: string }> {
  public fails(): GenericSchema {
    return { id: '' };  // <-- This line produce the error, see below
  }

  public works(): GenericSchema {
    return { id: '' } as GenericSchema;
  }
}
const model = new Model<{id:string, x:string}>();
const obj = model.works();
console.log(obj.x); //undefined, but supposed to be string, right?

Playground

Thanks a lot for the example, that makes a lot of sense!

In my real use case I'm accessing a database so it would be OK for obj.x to be out of sync, since this would be a developer error (providing the wrong type for the data model in the DB). That said, to me it now (with your help) seems the TS compile is handling the above example correctly.

Now, if I change the example to be a bit closer to a real use case with an external db driver module, it may look something like this. And now the compiler accepts the generic as a return type, without the type assertion - which also makes sense.

// TS don't know what's in the DB (obviously)
const dataInDb: any = {id: '', stuff: ''}

// DB Driver takes a generic which defines what is 
// in the DB (up to the developer to make sure the 
// generic match the data model) 
function readDb<Schema>():Schema {
  return dataInDb
}

class Model<Schema extends { id: string }> {
  // This now works without the type assertion
  public read(): Schema {
    return readDb<Schema>();
  }
}

const model = new Model<{id:string, x:string}>();
const obj = model.read();
console.log(obj.x); //undefined

Playground

A more contrived example:

class Model<GenericSchema extends { id: string }> {
  public fails(): GenericSchema {
    return { id: '' } as any;  // <-- This line does not produce any error anymore
  }

  public works(): GenericSchema {
    return { id: '' } as GenericSchema;
  }
}
const model = new Model<{id:string, x:string}>();
const obj = model.works();
console.log(obj.x); //undefined

Playground

In summary, my initial example was simply not correct and both examples works as expected. I'll close this issue and bring the matter back over to the @types/mongodb https://github.com/DefinitelyTyped/DefinitelyTyped/issues/39358 package.

Big thanks for your help @fatcerberus and @AnyhowStep!

Was this page helpful?
0 / 5 - 0 ratings

Related issues

weswigham picture weswigham  路  3Comments

jbondc picture jbondc  路  3Comments

remojansen picture remojansen  路  3Comments

kyasbal-1994 picture kyasbal-1994  路  3Comments

bgrieder picture bgrieder  路  3Comments