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

Generic constraints as return type in class methods #35077

Closed
lirbank opened this issue Nov 13, 2019 · 4 comments
Closed

Generic constraints as return type in class methods #35077

lirbank opened this issue Nov 13, 2019 · 4 comments
Labels
Working as Intended The behavior described is the intended behavior; this is not a bug

Comments

@lirbank
Copy link

lirbank commented Nov 13, 2019

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
DefinitelyTyped/DefinitelyTyped#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 #34567 and as such probably also #33014.

@fatcerberus
Copy link

fatcerberus commented Nov 13, 2019

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.

@AnyhowStep
Copy link
Contributor

AnyhowStep commented Nov 13, 2019

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

@lirbank
Copy link
Author

lirbank commented Nov 15, 2019

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 DefinitelyTyped/DefinitelyTyped#39358 package.

Big thanks for your help @fatcerberus and @AnyhowStep!

@lirbank lirbank closed this as completed Nov 15, 2019
@RyanCavanaugh
Copy link
Member

https://twitter.com/SeaRyanC/status/1121995986862084096

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Working as Intended The behavior described is the intended behavior; this is not a bug
Projects
None yet
Development

No branches or pull requests

4 participants