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
- Rename
IUser
toIUserDocument
. This is to separate your schema from your instance methods. - Import
Document
from mongoose. - Extend the interface from
Document
.
Model file
- Rename all instances of
IUser
toIUserDocument
, including the module path if you rename the file. - Rename only the definition of
IUserModel
toIUser
. - Change what
IUser
extends from, fromIUserDocument, Document
toIUserDocument
. - Create a new interface called
IUserModel
which extends fromModel<IUser>
. - Declare your static methods in
IUserModel
. - Change the
User
constant type fromModel<IUserModel>
toIUserModel
, asIUserModel
now extendsModel<IUser>
. - 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:
- Mine is an
instance
method while yours is astatic
method. I'm confident that there is a similar method access strategy. - 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 accessthis.password