44

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!

Jannis Lehmann
  • 1,428
  • 3
  • 17
  • 31

6 Answers6

97

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;
}
Matt Shipton
  • 1,256
  • 1
  • 11
  • 15
  • Amazing answer, Thanks a lot! Would you know how to mock IUserModel in Typescript? I have a class Foo that has IUserModel injected, and some of Foo's methods use IUserModel statics. I would like to inject a mock of IUserModel with fake statics, how could I do that? – kikar Oct 27 '17 at 15:55
  • many thanks for this answer, it took a day to find this answer but it worth. THANKS –  May 14 '18 at 15:45
  • 1
    This is for sure the right solution to the original problem! The accepted answer is really a work around. – leogoesger Jun 26 '18 at 18:16
  • 1
    As a little addition: if you have arrays in IUserDocument, I suggest to use `mongoose.Types.Array` as type of the property. This type contains extra methods (e.g. `addToSet`, `pull`) – gianlucaparadise Dec 03 '18 at 06:45
  • This should be the right way to use Mongoose with Typescript, I spent so long search around trying to find a solution for mongoose with ts, none of the tutorials online are complete, you should make a blog post regarding using mongoose with typescript using this anwser – Hansen W Aug 13 '19 at 04:32
  • I was struggling with `this` reporting as an `any` Type within you comparePassword function. This was my solution for that: ```UserSchema.methods.comparePassword = async function ( candidatePassword: string, ): Promise { return await bcrypt.compare(candidatePassword, this.password); };``` – Christopher Bull Apr 01 '20 at 15:10
  • Further to previous comment, you should also update the IUser interface to return a `Promise` type, e.g: ```export interface IUser extends UserDocument { comparePassword(password: string): Promise; }``` – Christopher Bull Apr 01 '20 at 15:18
  • if i remove```userSchema.static('hashPassword', (password: string): string => { return bcrypt.hashSync(password); });``` still vscode/typescript suggest me User.hashPassword method while it doesn't exists runtime. any solution from interface implements please ? – shivshankar Oct 16 '20 at 19:39
  • Although this answer didn't help me solve my problem, I am upvoting it as it is very informative, sort of a missing manual of extending symbols when working with (type|mon)goose. – user776686 Jan 06 '22 at 13:55
18

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
Amir Meyari
  • 573
  • 4
  • 9
  • 30
Nate May
  • 3,814
  • 7
  • 33
  • 86
  • That did actually work. Thanks a lot! One side question, why don't you use `user.comparePassword`? Maybe this will fix your `this.password` problem (https://stackoverflow.com/questions/42415142/mongoose-instance-properties-are-undefined-within-instance-methods) as I experienced a similar issue. – Jannis Lehmann Mar 16 '17 at 10:58
  • 1
    If I understand you question, the reason is that Typescript was throwing a compile error. – Nate May Mar 16 '17 at 19:14
  • 7
    I've also been struggling with this issue for awhile. To access a static schema method via Typescript, use `User.schema.statics.hashPassword()`. – Jackpile May 30 '17 at 17:42
11

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:

  • Extend Document to define types for instance properties and .method functions.
  • Extend the Model (of a Document) to define types for .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.

Max Wilder
  • 572
  • 6
  • 14
  • Thank you for the explanation. When an instance method is defined on a document in typescript with strict type checking I receive `Property 'checkPassword' does not exist on type 'User'` where User is my interface that extends Document. But if I change my variable (of type `User`) to `user.schema.methods.checkPassword` it doesn't complain due to it being any. Big picture, user.checkPassword complains, but user.schema.methods.checkPassword doesn't when my schema defines `UserSchema.methods.checkPassword = function(` Do I need to define that method in my interface? – Diesel Feb 03 '19 at 06:31
2

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

xerotolerant
  • 1,955
  • 4
  • 21
  • 39
  • I don't have access to the source right now, but I tried adding the method to the `IUserModel` but not to `IUser`. But would this even make a difference? Will try this later, nevertheless. – Jannis Lehmann Feb 27 '17 at 18:02
  • I'm not sure what the difference would be tbh, although the way you defined the methods are also different from how I do. I use the format `userSchema.methods.functionName = function(){}` I'm not sure if that is affecting you but it might be. – xerotolerant Feb 27 '17 at 18:20
  • I did change the way how I declared the methods due to some docs but this does not change. Will still test your answer as I get the time to do – Jannis Lehmann Feb 28 '17 at 18:26
  • 1
    Just out of curiosity, are you running the hashPassword method on the User class or an instance? – xerotolerant Mar 03 '17 at 02:37
  • Running it from the schema. I have no real User class. (`User: Model`) So I call it using `User.hashPassword(password)`. – Jannis Lehmann Mar 03 '17 at 13:27
0

So the one with 70 updates I also gave an upvote. But it is not a complete solution. He uses a trivial example based on the OP. However, more often than not when we use statics and methods in order to extend the functionality of the model, we want to reference the model itself. The problem with his solution is he using a callback function which means the value of this will not refer to the class context but rather a global.

The first step is to invoke the statics property rather than pass the property as an argument to the static function:

schema.statics.hashPassword

Now we cannot assign an arrow function to this member, for this inside the arrow function will still refer to the global object! We have to use function expression syntax in order to capture this in the context of the model:

schema.statics.hashPassword = async function(password: string): Promise<string> {
    console.log('the number of users: ', await this.count({}));
    ...
}
Daniel Viglione
  • 8,014
  • 9
  • 67
  • 101
0

Refer : https://mongoosejs.com/docs/typescript.html

  1. Just create an interface before the schema which represents a document structure.
  2. Add the interface type to the model.
  3. Export the model.

Quoting below from mongoose docs:

import { Schema, model, connect } from 'mongoose';

// 1. Create an interface representing a document in MongoDB.
interface User {
  name: string;
  email: string;
  avatar?: string;
}

// 2. Create a Schema corresponding to the document interface.
const schema = new Schema<User>({
  name: { type: String, required: true },
  email: { type: String, required: true },
  avatar: String
});

// 3. Create a Model.
const UserModel = model<User>('User', schema);

If you add any method to the schema, add its definition in the interface as well.