Reputation: 5611
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
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.
Upvotes: 17