
No Comments Yet
Be the first to share your thoughts and start the conversation.
A curious learner reached out on Discord to get his doubts clarified on Mongoose’s type definitions.
While what we did in previous lessons is the accurate and recommended approach by Mongoose itself, there is a little tiny issue. What’s that?
Let’s see.
In the latest versions, Mongoose introduced official support for TypeScript Bindings. It means that Mongoose has built-in TypeScript support without the need for external type definition files (@types/mongoose).
The recommended approach by Mongoose, as mentioned in their docs, is as this (the same thing that we did in throughout lessons)
import { Schema, model, connect } from 'mongoose';
// 1. Create an interface representing a document in MongoDB.
interface IUser {
name: string;
email: string;
avatar?: string;
}
// 2. Create a Schema corresponding to the document interface.
const userSchema = new Schema<IUser>({
name: { type: String, required: true },
email: { type: String, required: true },
avatar: String
});
// 3. Create a Model.
const User = model<IUser>('User', userSchema);
Pretty neat.
If you want to then use this IUser interface somewhere, you can use it by exporting it…
export interface IUser {
name: string;
email: string;
avatar?: string;
}
… but you won’t be then able to use Mongoose-specific fields such as _id or methods through this IUser interface. Why?
Because IUser never simply extends any kind of Mongoose model, it simply defines the types specified in the schema. The rest of the types are automatically inferred by Mongoose for you. Make sense?
So,
A straight-out-of-the-box thought would be to do this
export interface IUser extends Document {
name: string;
email: string;
avatar?: string;
}
This is valid and something many developers did before the latest versions of Mongoose. Now, the official Mongoose creators do not recommend doing this and will soon deprecate this way of defining types as it has performance implications.
Then,
If you scroll down a bit of the above shared github issues, you’ll see the recommended approach
Which is nothing but defining an Intersection Type,
export type IUserDoc = IUser & Document;
And if you want to stick with an interface, you can simply do,
export interface IUserDoc extends IUser, Document {}
Both of the above things work. It’s your choice.
So, let’s revisit all the models we’ve created and define a new type or interface that extends Mongoose Document so we can access fields like _id or id or any virtual methods provided by Mongoose on any kind of model.
How?
Simple,
import { Schema, models, model, Document } from "mongoose";
export interface IUser {
name: string;
username: string;
email: string;
bio?: string;
image?: string;
location?: string;
portfolio?: string;
reputation?: number;
}
export interface IUserDoc extends IUser, Document {}
const UserSchema = new Schema<IUser>(
{
name: { type: String, required: true },
username: { type: String, required: true, unique: true },
email: { type: String, required: true, unique: true },
bio: { type: String },
image: { type: String },
location: { type: String },
portfolio: { type: String },
reputation: { type: Number, default: 0 },
},
{ timestamps: true }
);
const User = models?.User || model<IUser>("User", UserSchema);
export default User;
Remember, we’re not touching the main model and changing its type. It stays as is!
Rather, we’re defining a whole new interface, IUserDoc, to solve our use case. Whenever we need to access any default Mongoose-specific fields, we’ll use IUserDoc to define types for that result and make it typesafe.
So, go ahead and update the types of your models accordingly. If you’re unsure how to proceed, I’ll leave the branch and types for all models here so you can cross-reference as needed.
User Model
Along with adding a new interface called IUserDoc, we made the image optional. This lets us create a user without an image at first and show one later using their initials instead.
import { Schema, models, model, Document } from "mongoose";
export interface IUser {
name: string;
username: string;
email: string;
bio?: string;
image?: string;
location?: string;
portfolio?: string;
reputation?: number;
}
export interface IUserDoc extends IUser, Document {}
const UserSchema = new Schema<IUser>(
{
name: { type: String, required: true },
username: { type: String, required: true, unique: true },
email: { type: String, required: true, unique: true },
bio: { type: String },
image: { type: String },
location: { type: String },
portfolio: { type: String },
reputation: { type: Number, default: 0 },
},
{ timestamps: true },
);
const User = models?.User || model<IUser>("User", UserSchema);
export default User;
Account Model
import { Schema, model, models, Types, Document } from "mongoose";
export interface IAccount {
userId: Types.ObjectId;
name: string;
image?: string;
password?: string;
provider: string; // e.g., 'github', 'google', 'credentials'
providerAccountId: string;
}
export interface IAccountDoc extends IAccount, Document {}
const AccountSchema = new Schema<IAccount>(
{
userId: { type: Schema.Types.ObjectId, ref: "User", required: true },
name: { type: String, required: true },
image: { type: String },
password: { type: String },
provider: { type: String, required: true },
providerAccountId: { type: String, required: true },
},
{ timestamps: true },
);
const Account = models?.Account || model<IAccount>("Account", AccountSchema);
export default Account;
Question Model
import { Schema, models, model, Types, Document } from "mongoose";
export interface IQuestion {
title: string;
content: string;
tags: Types.ObjectId[];
views: number;
answers: number;
upvotes: number;
downvotes: number;
author: Types.ObjectId;
}
export interface IQuestionDoc extends IQuestion, Document {}
const QuestionSchema = new Schema<IQuestion>(
{
title: { type: String, required: true },
content: { type: String, required: true },
tags: [{ type: Schema.Types.ObjectId, ref: "Tag" }],
views: { type: Number, default: 0 },
answers: { type: Number, default: 0 },
upvotes: { type: Number, default: 0 },
downvotes: { type: Number, default: 0 },
author: { type: Schema.Types.ObjectId, ref: "User", required: true },
},
{ timestamps: true },
);
const Question =
models?.Question || model<IQuestion>("Question", QuestionSchema);
export default Question;
Answer Model
import { Schema, models, model, Types, Document } from "mongoose";
export interface IAnswer {
author: Types.ObjectId;
question: Types.ObjectId;
content: string;
upvotes: number;
downvotes: number;
}
export interface IAnswerDoc extends IAnswer, Document {}
const AnswerSchema = new Schema<IAnswer>(
{
author: { type: Schema.Types.ObjectId, ref: "User", required: true },
question: {
type: Schema.Types.ObjectId,
ref: "Question",
required: true,
},
content: { type: String, required: true },
upvotes: { type: Number, default: 0 },
downvotes: { type: Number, default: 0 },
},
{ timestamps: true },
);
const Answer = models?.Answer || model<IAnswer>("Answer", AnswerSchema);
export default Answer;
Tag Model
import { Schema, models, model, Document } from "mongoose";
export interface ITag {
name: string;
questions: number;
}
export interface ITagDoc extends ITag, Document {}
const TagSchema = new Schema<ITag>(
{
name: { type: String, required: true, unique: true },
questions: { type: Number, default: 0 },
},
{ timestamps: true }
);
const Tag = models?.Tag || model<ITag>("Tag", TagSchema);
export default Tag;
Tag Question Model
import { Schema, models, model, Types, Document } from "mongoose";
export interface ITagQuestion {
tag: Types.ObjectId;
question: Types.ObjectId;
}
export interface ITagQuestionDoc extends ITagQuestion, Document {}
const TagQuestionSchema = new Schema<ITagQuestion>(
{
tag: { type: Schema.Types.ObjectId, ref: "Tag", required: true },
question: { type: Schema.Types.ObjectId, ref: "Question", required: true },
},
{ timestamps: true }
);
const TagQuestion =
models?.TagQuestion || model<ITagQuestion>("TagQuestion", TagQuestionSchema);
export default TagQuestion;
Vote Model
We can't create a new interface IVoteDoc if IVote includes an id field. This is because the Document interface already has an id field, which is a string, while the id field in IVote is of ObjectId type.
To avoid this conflict, we rename id to actionId and type to actionType. This allows us to extend the Document interface with the IVote interface smoothly.
import { Schema, models, model, Types, Document } from "mongoose";
export interface IVote {
author: Types.ObjectId;
actionId: Types.ObjectId;
actionType: string;
voteType: string;
}
export interface IVoteDoc extends IVote, Document {}
const VoteSchema = new Schema<IVote>(
{
author: { type: Schema.Types.ObjectId, ref: "User", required: true },
actionId: { type: Schema.Types.ObjectId, required: true },
actionType: { type: String, enum: ["question", "answer"], required: true },
voteType: { type: String, enum: ["upvote", "downvote"], required: true },
},
{ timestamps: true }
);
const Vote = models?.Vote || model<IVote>("Vote", VoteSchema);
export default Vote;
Collection Model
import { Schema, models, model, Types, Document } from "mongoose";
export interface ICollection {
author: Types.ObjectId;
question: Types.ObjectId;
}
export interface ICollectionDoc extends ICollection, Document {}
const CollectionSchema = new Schema<ICollection>(
{
author: { type: Schema.Types.ObjectId, ref: "User", required: true },
question: { type: Schema.Types.ObjectId, ref: "Question", required: true },
},
{ timestamps: true }
);
const Collection =
models?.Collection || model<ICollection>("Collection", CollectionSchema);
export default Collection;
Interaction Model
import { Schema, models, model, Types, Document } from "mongoose";
export interface IInteraction {
user: Types.ObjectId;
action: string;
actionId: Types.ObjectId;
actionType: string;
}
export interface IInteractionDoc extends IInteraction, Document {}
const InteractionSchema = new Schema<IInteraction>(
{
user: { type: Schema.Types.ObjectId, ref: "User", required: true },
action: { type: String, required: true }, // 'upvote', 'downvote', 'view', 'ask_question',
actionId: { type: Schema.Types.ObjectId, required: true }, // 'questionId', 'answerId',
actionType: { type: String, enum: ["question", "answer"], required: true },
},
{ timestamps: true }
);
const Interaction =
models?.Interaction || model<IInteraction>("Interaction", InteractionSchema);
export default Interaction;
Be the first to share your thoughts and start the conversation.