8

I try to create method hashPassword for user schema.

schema.method("hashPassword", function (): void {
  const salt = bcrypt.genSaltSync(10);
  const hash = bcrypt.hashSync(this.password, salt);

  this.password = hash;
});

And get an error Property 'password' does not exist on type 'Document<any>'. on password

Here is my file

import mongoose, { Schema, Document } from "mongoose";
import bcrypt from "bcryptjs";

/**
 * This interface should be the same as JWTPayload declared in types/global.d.ts file
 */
export interface IUser extends Document {
  name: string;
  email: string;
  username: string;
  password: string;
  confirmed: boolean;
  hashPassword: () => void;
  checkPassword: (password: string) => boolean;
}

// User schema
const schema = new Schema(
  {
    name: {
      type: String,
      required: true,
    },
    email: {
      type: String,
      required: true,
    },
    username: {
      type: String,
      required: true,
    },
    password: {
      type: String,
      required: true,
    },
    confirmed: {
      type: Boolean,
      default: false,
    },
  },
  { timestamps: true }
);

schema.method("hashPassword", function (): void {
  const salt = bcrypt.genSaltSync(10);
  const hash = bcrypt.hashSync(this.password, salt);

  this.password = hash;
});

// User model
export const User = mongoose.model<IUser>("User", schema, "users");

0

6 Answers 6

10

At the point where you define the method, the schema object doesn't know that it is the Schema for an IUser and not just any Document. You need to set the generic type for the Schema when you create it: new Schema<IUser>( ... ).

Sign up to request clarification or add additional context in comments.

1 Comment

Much simpler than other answers.
6

As suggested by one of the collaborators of mongoose, we can use the below way for creating instance methods:

const schema = new Schema<ITestModel, Model<ITestModel, {}, InstanceMethods>> // InstanceMethods would be the interface on which we would define the methods

schema.methods.methodName = function() {}

const Model = model<ITestModel, Model<ITestModel, {}, InstanceMethods>>("testModel", ModelSchema)

const modelInstance = new Model();
modelInstance.methodName() // works

link: https://github.com/Automattic/mongoose/issues/10358#issuecomment-861779692

Comments

5

This makes is Clear Enough

import mongoose, { Schema, Document, Model } from "mongoose";
import bcrypt from "bcrypt";

interface IUser {
    username: string;
    hashedPassword: string;
}

interface IUserDocument extends IUser, Document {
    setPassword: (password: string) => Promise<void>;
    checkPassword: (password: string) => Promise<boolean>;
}

interface IUserModel extends Model<IUserDocument> {
    findByUsername: (username: string) => Promise<IUserDocument>;
}

const UserSchema: Schema<IUserDocument> = new Schema({
    username: { type: String, required: true },
    hashedPassword: { type: String, required: true },
});

UserSchema.methods.setPassword = async function (password: string) {
    const hash = await bcrypt.hash(password, 10);
    this.hashedPassword = hash;
};

UserSchema.methods.checkPassword = async function (password: string) {
    const result = await bcrypt.compare(password, this.hashedPassword);
    return result;
};

UserSchema.statics.findByUsername = function (username: string) {
    return this.findOne({ username });
};

const User = mongoose.model<IUserDocument, IUserModel>("User", UserSchema);
export default User;

2 Comments

Great example, makes everything nice and clear.
Make sure not to use arrow functions when defining methods! I usually write arrow functions everywhere, but I forgot that "this" is not available in arrow functions. Took me couple of minutes to remember that.
2

You should declare an interface that extends Model like so:

    interface IUser {...}
    interface IUserInstanceCreation extends Model<IUser> {}

then declare your schema;

    const userSchema = new Schema<IUser, IUserInstanceCreation, IUser>({...})

This would as well ensure that the Schema follows the attributes in IUser.

1 Comment

I already figured it out but thank you anyway
2

Solution to avoid having to redefine types that are already in the schema

import { Schema, model, InferSchemaType } from "mongoose"
import bcrypt from "bcrypt"

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

userSchema.methods.verifyPassword = async function(password: string){
    return await bcrypt.compare(password, this.password)
}

declare interface IUser extends InferSchemaType<typeof userSchema> {
    verifyPassword(password: string): boolean
}

export const UserModel = model<IUser>("Users", userSchema)

Comments

0

As per mongoose docs To automatically infer type you need to define them in Schema object creation

something like this

// User schema
const schema = new Schema(
  {
    name: {
      type: String,
      required: true,
    },
    email: {
      type: String,
      required: true,
    },
    username: {
      type: String,
      required: true,
    },
    password: {
      type: String,
      required: true,
    },
    confirmed: {
      type: Boolean,
      default: false,
    },
  },
  {
    timestamps: true,
    methods: {
      async hashPassword() {
        const salt = bcrypt.genSaltSync(10);
        const hash = bcrypt.hashSync(this.password, salt);

        this.password = hash;
      }
    }
  }
);

export const User = mongoose.model<IUser>("User", schema, "users")

And simply use it

const user = new User({...})
user.hashPassword() // intellisense will detect it

No need to implicitly define any types/interfaces

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.