Definitelytyped: Recent updates to mongoose.d.ts broke use of interfaces on model compilation

Created on 14 Jul 2016  ·  35Comments  ·  Source: DefinitelyTyped/DefinitelyTyped

  • [x] I tried using the latest mongoose/mongoose.d.ts file in this repo and had problems.

    • The authors of that type definition are @simonxca in #10084.

Basically this broke my use of mongoose. Not that I am sure I was using it correctly as I'm a bit of a novice in this TS/JS realm.

Snippet:

{...}
interface Location extends mongoose.Document {
  name: string;
  address: string;
  rating: number;
  facilities: string[];
  coords: number[];
  openingTimes: any[];
  reviews: any[];
};
const locationSchema = new mongoose.Schema({
  name: { type: String, required: true },
  address: String,
  rating: { type: Number, "default": 0, min: 0, max: 5 },
  facilities: [String],
  coords: { type: [Number], index: "2dsphere" },
  openingTimes: [openingTimeSchema],
  reviews: [reviewSchema]
});

mongoose.model<Location>("Location", locationSchema);
{...}

So I can no longer specify the interface with additional properties within <>.

Supplied parameters do not match the signature of the call target.

Simplifying to:

mongoose.model("Location", locationSchema);

Works of course, but now in my controllers when executing a query and explicitly trying to access a property of a document TS is unaware of the additional properties on a document.

    .findById(req.params.locationid)
    .select("-reviews -rating")
    .exec(
      (err, location) => {
        if (!location) {
          sendJsonResponse(res, 404, {
            "message": "locationid not found"
          });
          return;
        } else if (err) {
          sendJsonResponse(res, 400, err);
          return;
        }
        location.name = req.body.name;
        location.address = req.body.address;
{...}

Property 'name' does not exist on type '_Model & EventEmitter'.

Then I thought perhaps creating a class that extends mongoose and implements my interface would work, but then I have to provide an implementation for everything in my interface and mongoose.Document. That seems kind of asinine so I figure I must be missing the most obvious way to do this.

Thoughts?

Most helpful comment

@Ruroni @digital216 @linusbrolin Heads up. Some changes are coming soon for Typescript 2. See here: https://github.com/simonxca/DefinitelyTyped/blob/patch-mongoose/mongoose/mongoose.d.ts
In particular, you will need to do this again: interface Location extends mongoose.Document {...}; Sorry for the inconvenience!

After talking with the guys at typings, I changed the implementation of mongoose.Model<T> similar to what it was before because it lets you do interface MyModel<T extends Document> extends mongoose.Model<T> {...}. Interfaces are easier to read than the type intersections I was using before.

I've added a README of how to use the definitions in case there are other changes that affect you. (I don't think so for now) https://github.com/simonxca/DefinitelyTyped/tree/patch-mongoose/mongoose

All 35 comments

@ruroni Okay I see, thanks for the examples. I'll add the type paramater back onto mongoose.Model<>, I removed it because I don't think there's a general Typescript way to do the above and the way we've done it is specific to how we implemented our previous definitions so it's rigid.

But, I figured it would break something so I'll add it back.

@Ruroni @digital216 can you try out https://github.com/simonxca/DefinitelyTyped/blob/model-fix/mongoose/mongoose.d.ts if anything is still broken, please send me code snippets to test.

Also, the interface should not extend mongoose.Document since Model.find() returns an array of mongoose.Model instances (mongoose.Model extends mongoose.Document so it should still work, but it's inaccurate and mongoose.Document does not have methods like save() or remove() or inherited EventEmitter methods).

Just use:

interface Location { ... }
mongoose.model<Location>("Location", LocationSchema)

The definitions will handle extending mongoose.Model transparently for you.

I know in the previous definitions we added a bunch of methods to interface Document { ... } that it didn't have as a hack so if enough people complain, then I'll figure something out for compatibility.

In the link click "Raw" and copy/paste.
You might have to change some of the references at the top to index.d.ts.
Ex:
///<reference path="../mongodb/mongodb.d.ts" />
To
///<reference path="../mongodb/index.d.ts" />

So far so good, I'm not running into any other issues (at least how I have it implemented)

Great, interfaces working again. Made the changes you suggested as well to great success.

Unrelated, but since DocumentArray is implemented in this new definition file:

class DocumentArray<T extends Document> extends Array<T>

I can assign the type mongoose.Types.DocumentArray<mongoose.Document> on one of my interface properties and get the expected functionality from the id() helper method: id(id: ObjectId | string | number | NativeBuffer): Embedded;

Thanks for getting these definitions updated!

@Ruroni @digital216 Sounds good thanks for the feedback!

Out of curiosity (I'm not familiar enough with the Typing thing yet). Would the typed Model have anything to do with it not respecting the unique property? I have a field that I've set the unique on, and it's allowing me to created documents with duplicate values in that field

I don't think it should. The type is just there to provide the compiler/IDE info about code completion and type checking, it doesn't change the behaviour. But just to be sure, can you post a simple code snippet here?

Regarding the save() and remove() methods that were previously available through extending Document, how should queries be constructed now instead?

Consider this, which worked before:

interface AddressType extends Document {
  // _id: Types.ObjectId; // implemented by Document
  addressType: string;
}

const AddressTypeSchema: Schema = new Schema({
  // _id: Schema.Types.ObjectId, // Not needed, MongoDB autogenerates this
  addressType: { type: String, required: true }
});


function saveAddressType(addressType: AddressType): Promise<AddressType> {
  return new Promise(
    function(resolve: (value: AddressType) => void, reject: (reason?: any) => void) {
      AddressTypeModel
      .findById(addressType._id)
      .exec(function(err: any, dbAddressType: AddressType) {
        if (err) {
          return reject(err);
        }

        // if dbAddressType is null or undefined, the addressType is new and should be added to the db
        if (!dbAddressType) {
          dbAddressType = new AddressTypeModel();
        }

        // update the addressType data
        dbAddressType.addressType = addressType.addressType;

        // save addressType
        dbAddressType.save(function(err2: any, resAddressType: AddressType) {
          if (err2) {
            return reject(err2);
          }

          // we return "resAddressType.toObject() as AddressType" here as "save" has no "lean" functionality
          return resolve(resAddressType.toObject() as AddressType);
        });
      });
    }
  );
}

Now, the save() method is no longer available.

Or this:

function deleteAddressType(id: string): Promise<AddressType> {
  // We must remove documents with this method, instead of findByIdAndRemove.
  // The reason is that custom hooks (pre/post functions) in the AddressTypeSchema will not be executed otherwise!
  // More information: http://mongoosejs.com/docs/middleware.html
  return AddressTypeModel
    .findById(id)
    .exec()
    .then((res: AddressType) => {
      if (!res) {
        return new Promise<AddressType>((resolve, reject) => reject('No entity found with id: "' + id + '".'));
      }

      return res.remove();
    });
}

Now, the remove() method is no longer available.

According to the official (javascript) mongoose docs, my code is fine, but not according to the new d.ts file.

I'm fine with updating my code so it works with the new type definitions, but I just need to know how since there is very little documentation for it.

@linusbrolin How are you defining your AddressTypeModel? It should be defined like this:

interface AddressType {
  addressType: string;
}

AddressTypeModel = mongoose.model<AddressType>('AddressType', AddressTypeSchema);

AddressTypeModel
.findById(addressType._id)
.exec(function (err, dbAddressType) {  // definitions will infer types for you
  ...
}

@linusbrolin You're partly right, this is fine according to the docs:

.findById(addressType._id)
.exec(function (err, dbAddressType) {
  dbAddressType.save();   // dbAddressType is actually a mongoose.model not mongoose.Document
}

But this is not:

.findById(addressType._id)
.exec(function (err: any, dbAddressType: AddressType) {
  dbAddressType.save();    // no save() method
}

Since AddressType extends Document, but Document does not have a save() or remove() method.
http://mongoosejs.com/docs/api.html#document-js

Yes, I've defined the AddressTypeModel like you described:

import { Document, Schema, Types, Model, model } from 'mongoose';

export interface AddressType extends Document {
  // _id: Types.ObjectId; // implemented by Document
  addressType: string;
}

export const AddressTypeSchema: Schema = new Schema({
  // _id: Schema.Types.ObjectId, // Not needed, MongoDB autogenerates this
  addressType: { type: String, required: true }
});

export var AddressTypeModel: Model<AddressType> = model<AddressType>('AddressType', AddressTypeSchema);

So, should dbAddressType in the query be of type AddressTypeModel instead of type AddressType?

If I do this, I get an error on the type:

.findById(addressType._id)
.exec(function (err: any, dbAddressType: AddressTypeModel) { // will get error: "Cannot find name 'AddressTypeModel'
  dbAddressType.save();
}

Okay good, try removing both types on err and dbAddressType the definitions should infer those types for you. Also the definitions extends Document for you so you can just use interface AddressType { ... } Let me know that gets an error.

But isn't the point of TypeScript to have typed variables?
Yes, the inferred types would work, resulting in:

(res: AddressType & _mongoose._Model<AddressType> & EventEmitter)

But then the code will not be as readable in a generic text editor.
One of the points of having typed variables is code readability.

If you could make the types public in the d.ts, then I could just add them in my code for readability, but for example:

_mongoose._Model<AddressType>

is not public in your d.ts file.

The type returned is mongoose.model<AddressType> if you need it.

@linusbrolin it's because mongoose.model<> extends Document and EventEmitter and whatever methods and properties you added to it. currently we can't have a single type for all of that.

@linusbrolin so we use mongoose.model<> as a type reference to combine them together.

Okay, I think I understand it now..
It's a bit more cumbersome than before, but I guess it's a bit more logical.

Thanks for explaining it. :)

@linusbrolin yea, it is more cumbersome heh :) I know other libraries do this and it's never readable when you hover over in the editor.
Would be better if the editor would show the type reference instead of all the expanded types when you hover over.

@Ruroni @digital216 @linusbrolin Heads up. Some changes are coming soon for Typescript 2. See here: https://github.com/simonxca/DefinitelyTyped/blob/patch-mongoose/mongoose/mongoose.d.ts
In particular, you will need to do this again: interface Location extends mongoose.Document {...}; Sorry for the inconvenience!

After talking with the guys at typings, I changed the implementation of mongoose.Model<T> similar to what it was before because it lets you do interface MyModel<T extends Document> extends mongoose.Model<T> {...}. Interfaces are easier to read than the type intersections I was using before.

I've added a README of how to use the definitions in case there are other changes that affect you. (I don't think so for now) https://github.com/simonxca/DefinitelyTyped/tree/patch-mongoose/mongoose

@simonxca Sounds good.
Would you mind letting me know once it's been released, so I can update my code then?

@linusbrolin alright it's been updated you can give it a try and let me know if you get any errors

@simonxca Alright, I've modified my code, but I get a few errors.
For example:
I use both passport-local-mongoose and mongoose-paginate in my project, and use both of them in some models and only one of them in other models.
The models where I only use mongoose-paginate get this error:

import { PaginateModel, Document, Schema, model } from 'mongoose';
import * as mongoosePaginate from 'mongoose-paginate';

interface Test extends Document {
  _id: string;
  test: string;
}

const TestSchema: Schema = new Schema({
  // _id: Schema.Types.ObjectId, // MongoDB autogenerates this
  test: String,
});
TestSchema.plugin(mongoosePaginate);

interface TestModel<T extends Document> extends PaginateModel<T> {}

var TestModel: TestModel<Test> = model<Test>('Test', TestSchema);

// Error message:
// Type 'PassportLocalModel<Test>' is not assignable to type 'TestModel<Test>'.
//   Property 'paginate' is missing in type 'PassportLocalModel<Test>'.

In the User model, where I do use both passport-local-document and mongoose-paginate, I get the following errors

import { PassportLocalModel, PassportLocalDocument, PassportLocalSchema, PassportLocalOptions, PaginateModel, Schema, model } from 'mongoose';
import * as passportLocalMongoose from 'passport-local-mongoose';
import * as mongoosePaginate from 'mongoose-paginate';

interface User extends PassportLocalDocument {
  _id: string;
  username: string;
  password: string;
}

const UserSchema: PassportLocalSchema = new Schema({
  // _id: Schema.Types.ObjectId, // MongoDB autogenerates this
  username: String,
  password: String
});

let options: PassportLocalOptions = <PassportLocalOptions>{};
options.usernameField = 'username';
options.hashField = 'password';
options.usernameLowerCase = true;

UserSchema.plugin(passportLocalMongoose, options);
UserSchema.plugin(mongoosePaginate);

interface UserModel<T extends PassportLocalDocument> extends PassportLocalModel<T>, PaginateModel<T> {}

// Error message 1:
// Type 'T' does not satisfy the constraint 'Document'.
//   Type 'PassportLocalDocument' is not assignable to type 'Document'.
//     Types of property 'increment' are incompatible.
//       Type '() => PassportLocalDocument' is not assignable to type '() => T'.
//         Type 'PassportLocalDocument' is not assignable to type 'T'.

// Error message 2:
// Type 'T' does not satisfy the constraint 'Document'.
//   Type 'PassportLocalDocument' is not assignable to type 'Document'.

var UserModel: UserModel<User> = model<User>('User', UserSchema);

// Error message:
// Type 'PassportLocalModel<User>' is not assignable to type 'UserModel<User>'.
//   Property 'paginate' is missing in type 'PassportLocalModel<User>'.

The 'increment' error is probably from a third plugin I use on some other models, mongoose-sequence.
That plugin still doesn't have typings though.
However, that shouldn't cause it to mess up the User model, which doesn't use that plugin at all.

@linusbrolin Thanks for the examples. Looking into this.

@linusbrolin Looks like Typescript is getting confused by all the overrides to mongoose.model().
I believe you can tell mongoose.model() exactly what to return in the second type parameter.

In your first example change:
var TestModel: TestModel<Test> = model<Test>('Test', TestSchema);
to
var TestModel: TestModel<Test> = model<Test, TestModel<Test>>('Test', TestSchema);

In your second example change:
var UserModel: UserModel<User> = model<User>('User', UserSchema);
to
var UserModel: UserModel<User> = model<User, UserModel<User>>('User', UserSchema);

Give that a try and see if those errors go away.

@linusbrolin In the definitions, I had to change:
interface PassportLocalModel<T extends PassportLocalDocument> extends Model<T> {}
to
interface PassportLocalModel<T extends Document> extends Model<T> {}

The first version works for Typescript 2.x but not for Typescript 1.x apparently and will cause errors when I commit to DefinitelyTyped.

I believe you can fix this error by either:
import {Document} from 'mongoose' at the top, and change:
interface UserModel<T extends PassportLocalDocument> extends PassportLocalModel<T>, PaginateModel<T>
to
interface UserModel<T extends Document> extends PassportLocalModel<T>, PaginateModel<T>

or by upgrading your Typescript compiler to version 2.

UserModel<User> should still have the static methods of PassportLocalModel<User> and PaginateModel<User> and will return User, which extends PassportLocalDocument so I don't think you would lose type information by doing this.

@simonxca that seems to have fixed the errors, great! :)

I have another question, though. ;)
If the mongoose plugins have typed options that need to be provided when registering them, how do I make those options known and available, without them interfering with other plugins or regular mongoose?

For example, I'm building type definitions for the mongoose-sequence plugin, which look like this:

// Type definitions for mongoose-sequence 3.0.2
// Project: https://github.com/ramiel/mongoose-sequence
// Definitions by: Linus Brolin <https://github.com/linusbrolin/>

/// <reference path="../mongoose.d.ts" />

declare module 'mongoose' {
  export interface SequenceOptions {
    inc_field: string;                  // The name of the field to increment. Mandatory, default is _id
    id?: string;                        // Id of the sequence. Is mandatory only for scoped sequences but its use is strongly encouraged.
    reference_fields?: Array<string>;   // The field to reference for a scoped counter. Optional
    disable_hooks?: boolean;            // If true, the counter will not be incremented on saving a new document. Default to false
    collection_name?: string;           // By default the collection name to mantain the status of the counters is counters. You can override it using this option
  }

  interface SequenceModel<T extends Document> extends Model<T> {
    setNext(sequenceId: string, callback: (err: any, res: T) => void): void;
  }

  export interface SequenceSchema extends Schema {
    plugin(
      plugin: (schema: SequenceSchema, options: SequenceOptions) => void,
      options: SequenceOptions
    ): this;

    // why do I need this here, when it already exists in Schema and should be available as an overload anyway?
    plugin(plugin: (schema: Schema, options?: Object) => void, opts?: Object): this;
  }

  export function model<T extends Document>(
    name: string,
    schema?: Schema,
    collection?: string,
    skipInit?: boolean): SequenceModel<T>;

  export function model<T extends Document, U extends SequenceModel<T>>(
    name: string,
    schema?: Schema,
    collection?: string,
    skipInit?: boolean): U;
}

declare module 'mongoose-sequence' {
  import mongoose = require('mongoose');
  var _: (schema: mongoose.Schema, options?: Object) => void;
  export = _;
}

If I remove the overload plugin line:

plugin(plugin: (schema: Schema, options?: Object) => void, opts?: Object): this;

then I get this error if I try to load a different plugin on the same schema that is using monoose-sequence:

import { SequenceModel, SequenceOptions, SequenceSchema, PaginateModel, Document, Schema, model } from 'mongoose';
import * as mongooseSequence from 'mongoose-sequence';
import * as mongoosePaginate from 'mongoose-paginate';

interface Test extends Document {
  _id: string;
  incNumber: number;
}

const TestSchema: SequenceSchema = new Schema({
  // _id: Schema.Types.ObjectId, // MongoDB autogenerates this
  incNumber: Number
});

// let seqOpts: SequenceOptions = { inc_field: 'incNumber' };
TestSchema.plugin(mongooseSequence, { inc_field: 'incNumber' });
TestSchema.plugin(mongoosePaginate);

// Error message for mongoosePaginate:
// [ts] Supplied parameters do not match any signature of call target.
// (method) SequenceSchema.plugin(plugin: (schema: SequenceSchema, options: SequenceOptions) => void, options: SequenceOptions): SequenceSchema

interface TestModel<T extends Document> extends SequenceModel<T>, PaginateModel<T> {}

var TestModel: TestModel<Test> = model<Test, TestModel<Test>>('Test', TestSchema);

@linusbrolin Hmm I asked StackOverflow and apparently it simply doesn't work in Typescript.
http://stackoverflow.com/questions/39460333/typescript-method-overloading-in-subclass/39460361#answer-39460371

So you'll have to provide the parent class's signature for plugin()

@simonxca yeah, I figured that's how it was.. Bummer. :/
Seems silly, hopefully Typescript will make it work in later versions.

In the mean time, it means you should probably update your mongoose.d.ts plugin guide to include this information.

I will update passport-local-mongoose.d.ts to include the overload, and publish the mongoose-sequence.d.ts

@simonxca the version of mongoose.d.ts at DefinitelyTyped is not your last version
https://github.com/DefinitelyTyped/DefinitelyTyped/blob/types-2.0/mongoose/index.d.ts
So when I do sudo npm install --save @types/mongoose I get this old version, with which I have the problem with the save method not being in my interface extending Document.

Do you know when will the version be updated?

@camilleblanc I'm not sure how types-2.0 branch gets updated. Looking into it.

@simonxca thanks, still waiting for your update on that.
I also have another problem with how to declare sub documents.
I have this kind of code:

export interface MySubEntity {
    property: string;
    property2: string;
}

export interface MyEntity extends mongoose.Document {
    name: string;
    sub: MySubEntity[];
}

var mySubEntitySchema = new Schema({
  property1: String,
  property2: String
});

var myEntitySchema = new Schema({
  name: String,
  sub: [mySubEntitySchema]
});

export var MyEntityModel: mongoose.Model<MyEntityDocument> = mongoose.model<MyEntityDocument>("MyEntity", myEntitySchema);

Now somewhere I have the _id of a MySubEntity, and I have retrieved the MyEntity where it is.
So to retrieve my sub entity, I'm filtering on the sub array with the _id:

var mySubSearched = _.find(myEntity.sub, function (sub) { return sub._id == mySubId });

The problem is: _id is not a property of the interface MySubEntity, but it has one because mongo creates an _id for subdocuments.

So my question is: what is the correct way to declare sub documents?
My first thought was obviously to make the interface MySubEntity to extend mongoose.Document, but then I have another problem somewhere else: when I want to declare in my code a new MySubEntity... well it asks for all the properties of mongoose.Document that are usually created by mongo!

I hope my post is clear enough to understand!
Thanks in advance if you can enlighten me here, I'm really confused now.

@camilleblanc sorry was busy this week. Looks like it gets auto-updated from what I've read, but doesn't look like mongoose is up to date. I followed up with them here: https://github.com/DefinitelyTyped/DefinitelyTyped/issues/11210#issuecomment-247729698

@camilleblanc also this thread is getting too long. Can you please open a new issue with your problem above? I'll respond there.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

fasatrix picture fasatrix  ·  3Comments

jrmcdona picture jrmcdona  ·  3Comments

JudeAlquiza picture JudeAlquiza  ·  3Comments

csharpner picture csharpner  ·  3Comments

Zzzen picture Zzzen  ·  3Comments