Create mongoose schema methods using TypeScript

Viewed 4857

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");

5 Answers

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>( ... ).

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

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.

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;

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)

Related