Mongoose: Pre save hook not working on latest ts-node version

Created on 14 Jul 2018  路  6Comments  路  Source: Automattic/mongoose

Do you want to request a feature or report a bug?
Bug (maybe)

What is the current behavior?
Pre save hook is not working correctly on latest ts-node version.

If the current behavior is a bug, please provide the steps to reproduce.
This is my schema:

export const UserSchema = new Schema({
    email: { type: String, lowercase: true },
    password: String,
}); 
UserSchema.pre('save', function (next) {
    const user = this;
    console.log(user.email);
    next();
});
export const User = model<IUser>('User', UserSchema);

My IUser interface includes email and password too.

If I want to compile this script with ts-node following TSError during compilation:
Property 'email' does not exist on type 'Document'.

What is the expected behavior?
this should work as listed in the documentation.

Please mention your node.js, mongoose and MongoDB version.
node.js version: 8.11.3
mongoose version: 5.2.3
@types/mongoose: 5.2.0
ts-node: 7.0.0
typescript: 2.9.2
MongoDB version: 3.6.2

Most helpful comment

from this page in the typescript docs you can pass the value of this as the first parameter:

try this instead and see if that gets rid of the linter errors:

UserSchema.pre("save", function(this: IUser, next) {
  const user = this;
  console.log(user.email);
  next();
});

my example with this change worked as well.

closing this as it isn't related to mongoose. If you want continue the conversation please feel free to join us on Gitter.im or Slack.

All 6 comments

I have no practical experience with typescript so please excuse any obvious mistakes. Here is an example that I got to work:

NOTE that in usermodel.ts my pre save function has the ts interface

user.ts

export interface IUser {
  email?: string;
  firstName?: string;
  lastName?: string;
}

usermodel.ts

import { Document, Schema, Model, model } from "mongoose";
import { IUser } from "./user";

export interface IUserModel extends IUser, Document {
  fullName(): string;
}

export var UserSchema: Schema = new Schema({
  email: String,
  firstName: String,
  lastName: String
});

UserSchema.pre("save", function<IUserModel>(next) {
  const user = this;
  console.log(user.email);
  next();
});

UserSchema.methods.fullName = function(): string {
  return this.firstName.trim() + " " + this.lastName.trim();
};

const User: Model<IUserModel> = model<IUserModel>("User", UserSchema);

export default User

index.ts

import * as mongoose from 'mongoose';
const conn = mongoose.connection;

import User from './usermodel';

mongoose.connect('mongodb://localhost:27017/tsTest', { useNewUrlParser: true })

const user = new User({
  email: '[email protected]',
  firstName: 'Billy',
  lastName: 'TheKid'
})

async function run() {
  console.log(mongoose.version)
  await conn.dropDatabase()
  await user.save()
  let found = await User.findOne()
  console.log(found)
  return conn.close()
}

run()

Output:

6725: ./node_modules/.bin/ts-node ./index.ts
5.2.3
[email protected]
{ _id: 5b4b4c03123f3a1d7de3436d,
  email: '[email protected]',
  firstName: 'Billy',
  lastName: 'TheKid',
  __v: 0 }
6725:

I used this blog post for reference

Thanks a lot.
Adding <IUser> to my pre function fixed it for me.

UserSchema.pre('save', function <IUser>(next) { // ... }

But also there are existing now some linting warnings.
[ts] 'IUser' is declared but its value is never read.
and
[tslint] Shadowed name: 'IUser' (no-shadowed-variable)

My research on what <IUser> on a function will do resulted in the following.
https://www.typescriptlang.org/docs/handbook/generics.html
So I don't get it why this is fixed now. Is this an intended behavior?

from this page in the typescript docs you can pass the value of this as the first parameter:

try this instead and see if that gets rid of the linter errors:

UserSchema.pre("save", function(this: IUser, next) {
  const user = this;
  console.log(user.email);
  next();
});

my example with this change worked as well.

closing this as it isn't related to mongoose. If you want continue the conversation please feel free to join us on Gitter.im or Slack.

Nice job with this @lineus :+1:

For me, the issue turned out to be the context of this was changing.

For example, this code runs fine:

UserSchema.pre("save", function(this: IUser, next: any): void {

  const user = this;

  // Generate the salt
  bcrypt.genSalt(+config.saltWorkFactor, function(error: Error, salt): void {
    if (error) return next(error);
    let password = user.password;

    // Hash the password
    bcrypt.hash(password, salt, function(error, hash): void {
      if (error) return next(error);
      password = hash;
      next();
    });
  });
});

But this code does not:

UserSchema.pre("save", function(this: IUser, next: any): void {

  // Generate the salt
  bcrypt.genSalt(+config.saltWorkFactor, function(error: Error, salt): void {
    if (error) return next(error);
    let password = this.password;

    // Hash the password using the salt
    bcrypt.hash(password, salt, function(error, hash): void {
      if (error) return next(error);
      password = hash;
      next();
    });
  });
});

@devnoot you're right, you need to be careful to not lose function context when using callbacks. But why are you using callbacks when you're using typescript? I'm pretty baffled :confused:

Was this page helpful?
0 / 5 - 0 ratings