Typescript mongoose static model method "Property does not exist on type"

I was having the same problem as you, and then finally managed to resolve it after reading the documentation in the TS mongoose typings (which I didn't know about before, and I'm not sure how long the docs have been around), specifically this section.


As for your case, you'll want to follow a similar pattern to what you currently have, although you'll need to change a few things in both files.

IUser file

  1. Rename IUser to IUserDocument. This is to separate your schema from your instance methods.
  2. Import Document from mongoose.
  3. Extend the interface from Document.

Model file

  1. Rename all instances of IUser to IUserDocument, including the module path if you rename the file.
  2. Rename only the definition of IUserModel to IUser.
  3. Change what IUser extends from, from IUserDocument, Document to IUserDocument.
  4. Create a new interface called IUserModel which extends from Model<IUser>.
  5. Declare your static methods in IUserModel.
  6. Change the User constant type from Model<IUserModel> to IUserModel, as IUserModel now extends Model<IUser>.
  7. Change the type argument on your model call from <IUserModel> to <IUser, IUserModel>.

Here's what your model file would look like with those changes:

import * as bcrypt from 'bcryptjs';
import { Document, Schema, Model, model } from 'mongoose';

import { IUserDocument } from '../interfaces/IUserDocument';

export interface IUser extends IUserDocument {
    comparePassword(password: string): boolean; 
}

export interface IUserModel extends Model<IUser> {
    hashPassword(password: string): string;
}

export const userSchema: Schema = new Schema({
    email: { type: String, index: { unique: true }, required: true },
    name: { type: String, index: { unique: true }, required: true },
    password: { type: String, required: true }
});

userSchema.method('comparePassword', function (password: string): boolean {
    if (bcrypt.compareSync(password, this.password)) return true;
    return false;
});

userSchema.static('hashPassword', (password: string): string => {
    return bcrypt.hashSync(password);
});

export const User: IUserModel = model<IUser, IUserModel>('User', userSchema);

export default User;

And your (newly renamed) ../interfaces/IUserDocument module would look like this:

import { Document } from 'mongoose';

export interface IUserDocument extends Document {
    email: string;
    name: string;
    password: string;
}

I think you are having the same issue that I just struggled with. This issue is in your call. Several tutorials have you call the .comparePassword() method from the model like this.

User.comparePassword(candidate, cb...)

This doesn't work because the method is on the schema not on the model. The only way I was able to call the method was by finding this instance of the model using the standard mongoose/mongo query methods.

Here is relevant part of my passport middleware:

passport.use(
  new LocalStrategy({
    usernameField: 'email'
  },
    function (email: string, password: string, done: any) {
      User.findOne({ email: email }, function (err: Error, user: IUserModel) {
        if (err) throw err;
        if (!user) return done(null, false, { msg: 'unknown User' });
        user.schema.methods.comparePassword(password, user.password, function (error: Error, isMatch: boolean) {
          if (error) throw error;
          if (!isMatch) return done(null, false, { msg: 'Invalid password' });
          else {
            console.log('it was a match'); // lost my $HÏT when I saw it
            return done(null, user);
          }
        })
      })
    })
);

So I used findOne({}) to get the document instance and then had to access the schema methods by digging into the schema properties on the document user.schema.methods.comparePassword

A couple of differences that I have noticed:

  1. Mine is an instance method while yours is a static method. I'm confident that there is a similar method access strategy.
  2. I found that I had to pass the hash to the comparePassword() function. perhaps this isn't necessary on statics, but I was unable to access this.password