mrks
mrks

Reputation: 5611

Typescript and Mongoose: Property 'x' does not exist on type 'Document'

This is my Mongoose model that I use together with TypeScript:

import mongoose, { Schema } from "mongoose";

const userSchema: Schema = new Schema(
  {
    email: {
      type: String,
      required: true,
      unique: true,
      lowercase: true,
    },
    name: {
      type: String,
      maxlength: 50,
    },
    ...
    ...
  }
);

userSchema.method({
  transform() {
    const transformed = {};
    const fields = ["id", "name", "email", "createdAt", "role"];

    fields.forEach((field) => {
      transformed[field] = this[field];
    });
    return transformed;
  },
});

userSchema.statics = {
  roles,
  checkDuplicateEmailError(err: any) {
    if (err.code === 11000) {
      var error = new Error("Email already taken");
      return error;
    }

    return err;
  },
};

export default mongoose.model("User", userSchema);

I use this model in my controller:

import { Request, Response, NextFunction } from "express";
import User from "../models/user.model";
import httpStatus from "http-status";

export const register = async (
  req: Request,
  res: Response,
  next: NextFunction
) => {
  try {
    const user = new User(req.body);
    const savedUser = await user.save();
    res.status(httpStatus.CREATED);
    res.send(savedUser.transform());
  } catch (error) {
    return next(User.checkDuplicateEmailError(error));
  }
};

I get the following errors:

Property 'transform' does not exist on type 'Document'.

Property 'checkDuplicateEmailError' does not exist on type 'Model<Document, {}>'.

I tried export default mongoose.model<any>("User", userSchema); and I do not get the transform error but still the error for checkDuplicateEmailError.

Upvotes: 9

Views: 12136

Answers (1)

Linda Paiste
Linda Paiste

Reputation: 42160

You know that mongoose.model("User", userSchema); creates a Model, but the question is: a model of what?

Without any type annotations, the model User gets the type Model<Document, {}> and the user object created from new User() gets the type Document. So of course you are going to get errors like "Property 'transform' does not exist on type 'Document'."

When you added your <any> variable, the type for user became any. Which actually gives us less information than knowing that user is a Document.

What we want to do is create a model for specific type of Document describing our user. Instances of the user should have a method transform() while the model itself should have the method checkDuplicateEmailError(). We do this by passing generics to the mongoose.model() function:

export default mongoose.model<UserDocument, UserModel>("User", userSchema);

The hard part is figuring out those two types. Frustratingly, mongoose doesn't automatically apply the fields from your schema as properties of the type, though there are packages that do this. So we have to write them out as typescript types.

interface UserDocument extends Document {
  id: number;
  name: string;
  email: string;
  createdAt: number;
  role: string;
  transform(): Transformed;
}

Our transform function returns a object with 5 specific properties from the UserDocument. In order to access the names of those properties without having to type them again, I moved the fields from inside your transform method to be a top-level property. I used as const to keep their types as string literals rather than just string. (typeof transformFields)[number] gives us the union of those strings.

const transformFields = ["id", "name", "email", "createdAt", "role"] as const;

type Transformed = Pick<UserDocument, (typeof transformFields)[number]>

Our UserModel is a Model of UserDocument and it also includes our checkDuplicateEmailError function.

interface UserModel extends Model<UserDocument> {
  checkDuplicateEmailError(err: any): any;
}

We should also add the UserDocument generic when we create our Schema, so that this will have the type UserDocument when we access it inside a schema method.

const userSchema = new Schema<UserDocument>({

I got all sorts of typescript errors trying to implement the transform() method including missing index signatures. We can avoid reinventing the wheel here by using the pick method from lodash. I still had problems with the mongoose methods() helper function, but it works fine using the direct assignment approach.

userSchema.methods.transform = function (): Transformed {
  return pick(this, transformFields);
};

You could also use destructuring to avoid the index signature issues.

userSchema.methods.transform = function (): Transformed {
  const {id, name, email, createdAt, role} = this;
  return {id, name, email, createdAt, role};
}

Within your email check function, I added a typeof check to avoid runtime errors from trying to access the property err.code if err is undefined.

if ( typeof err === "object" && err.code === 11000) {

That should fix all your errors.

Playground Link

Upvotes: 17

Related Questions