Definitelytyped: [mongoose] mongoose.Model<...> does not satisfy the constraint Document

Created on 12 Mar 2019  路  7Comments  路  Source: DefinitelyTyped/DefinitelyTyped

Version:

  • @types/mongoose: 4.7.44

Code

interface Foo extends mongoose.Document {
  bar: number;
}

type Entry = mongoose.Model<Foo>;

Expected behavior:
It should be fine.

Actual behavior:
I get the error Type 'Foo' does not satisfy the constraint 'Document'.

With version 4.7.43 the code works.

  • [x] I tried using the "@types/[email protected] package and had problems.
  • [x] I tried using the latest stable version of tsc. https://www.npmjs.com/package/typescript
  • [x] [Mention](https://github.com/blog/821-mention-somebody-they-re-notified) the authors (see Definitions by: in index.d.ts) so they can respond.

    • @simonxca

    • @horiuchi

    • @lukasz-zak

    • @murbanowicz

Most helpful comment

You could do something like this to reuse "Entry" type if needed :)

interface Foo extends mongoose.Document {
  bar: number;
}

type Entry = mongoose.Model<Foo & Document>;

All 7 comments

I'm running into this too. This is the change by @murbanowicz.

Changing this line to:

    /**
     * Returns another Model instance.
     * @param name model name
     */
    model(name: string): Model<Document>;

Fixes this issue... Although I admit I'm not entirely sure why.
```

You also need --strictFunctionTypes in order to reproduce this.

This is perhaps a simpler example;

interface Document {
    model(name: string): Model<this>;
}

interface Model<T extends Document> {
    new (doc?: Partial<T>): T;
}

interface UserDocument extends Document {
    name: string;
}

const aUserDocument : UserDocument = {} as any;
const aDocument : Document = aUserDocument;

Here you'll get an error on the last line, because you can't assign UserDocument to Document. The circular reference between Document and Model makes reasoning about this a little maddening (and is possibly the root of the problem), but basically aUserDocument.model('foo') returns a Model<UserDocument>, which has a constructor that takes a Partial<UserDocument>. But, aDocument.model('foo') returns a Model<Document>, which instead has a constructor that takes a Partial<Document>. In order for these constructors to be assignable to each other, UserDocument would have to be assignable to Document... but that's back to square one, so we'd have to recurse infinitely to decide if these were assignable to each other or not.

Fortunately for us, the definition of model(name: string): Model<this>; is incorrect. If you do:

mongoose = require('mongoose');
Schema = mongoose.Schema;
catSchema = new Schema({name: String});
Cat = mongoose.model('Cat', catSchema);
dogSchema = new Schema({tag: String});
Dog = mongoose.model('Dog', dogSchema);
doc = new Cat({name: 'stashy'});

// Prints [ 'tag', '_id', '__v' ]
console.log(Object.keys(doc.model('Dog').schema.paths));

So here, even though doc is a CatDocument, calling doc.model('Dog') does not return a Model<CatDocument>. If you change this to:

    model<T extends Document>(name: string): Model<T>;

it fixes the problem.

Working on a PR...

You could do something like this to reuse "Entry" type if needed :)

interface Foo extends mongoose.Document {
  bar: number;
}

type Entry = mongoose.Model<Foo & Document>;

You could do something like this to reuse "Entry" type if needed :)

interface Foo extends mongoose.Document {
  bar: number;
}

type Entry = mongoose.Model<Foo & Document>;

You helped me a lot, thanks!

Has anyone trying writing a test for it?
Addressing @jwalton example:

interface UserDocument extends Document { name: string; }

When making the expected/mocked return object from the Model, one should mock the UserDocument interface object and so needs to reproduce the mongoose.Document properties it extends.
What is the best practice for it? what if you code is using some of the properties like .toJSON/toObject/save, these are also need to be mocked.

What am I missing?

Was this page helpful?
0 / 5 - 0 ratings