I am currently trying to add a static method to my mongoose schema but I can't find the reason why it doesn't work this way.
My model:
import * as bcrypt from 'bcryptjs';
import { Document, Schema, Model, model } from 'mongoose';
import { IUser } from '../interfaces/IUser';
export interface IUserModel extends IUser, Document {
comparePassword(password: string): boolean;
}
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: Model<IUserModel> = model<IUserModel>('User', userSchema);
export default User;
IUser:
export interface IUser {
email: string;
name: string;
password: string;
}
If I now try to call User.hashPassword(password)
I am getting the following error [ts] Property 'hashPassword' does not exist on type 'Model<IUserModel>'.
I know that I didn't define the method anywhere but I don't really know where I could put it as I can't just put a static method into an interface. I hope you can help my find the error, thanks in advance!
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
IUser
to IUserDocument
. This is to separate your schema from your instance methods.Document
from mongoose.Document
.Model file
IUser
to IUserDocument
, including the module path if you rename the file.IUserModel
to IUser
.IUser
extends from, from IUserDocument, Document
to IUserDocument
.IUserModel
which extends from Model<IUser>
.IUserModel
.User
constant type from Model<IUserModel>
to IUserModel
, as IUserModel
now extends Model<IUser>
.<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:
instance
method while yours is a static
method. I'm confident that there is a similar method access strategy.comparePassword()
function. perhaps this isn't necessary on statics, but I was unable to access this.password
For future readers:
Remember that we are dealing with two different Mongo/Mongoose concepts: a Model, and Documents.
Many Documents can be created from a single Model. The Model is the blueprint, the Document is the thing created according to the Model's instructions.
Each Document contains its own data. Each also carries their own individual instance methods which are tied to its own this
and only operate on that one specific instance.
The Model can have 'static' methods which are not tied to a specific Document instance, but operate over the whole collection of Documents.
How this all relates to TypeScript:
.method
functions..static
functions.The other answers here have decent code, so look at them and trace through the differences between how Documents are defined and how Models are defined.
And remember when you go to use these things in your code, the Model is used to create new Documents and to call static methods like User.findOne
or your custom statics (like User.hashPassword
is defined above).
And Documents are what you use to access the specific data from the object, or to call instance methods like this.save
and custom instance methods like this.comparePassword
defined above.
I cannot see your IUser interface however I suspect that you have not included the methods in there. EG
export interface IUser {
email: string,
hash: string,
salt: string,
setPassword(password: string): void,
validPassword(password: string): boolean,
generateJwt(): string
}
typescript will then recognize your methods and stop complaining
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With