Using Mongoose with TypeScript provides type safety, better development experience, and code hints. By using Mongoose's type definitions, errors can be caught at compile time.
Basic Type Definitions
Define Schema Types
typescriptimport mongoose, { Schema, Document, Model } from 'mongoose'; // Define document interface interface IUser extends Document { name: string; email: string; age: number; createdAt: Date; } // Define Schema const userSchema: Schema = new Schema({ name: { type: String, required: true }, email: { type: String, required: true, unique: true }, age: { type: Number, min: 0 }, createdAt: { type: Date, default: Date.now } }); // Create model type interface IUserModel extends Model<IUser> { findByEmail(email: string): Promise<IUser | null>; } // Create model const User: IUserModel = mongoose.model<IUser, IUserModel>('User', userSchema);
Using Typed Models
Creating Documents
typescriptconst user: IUser = new User({ name: 'John Doe', email: 'john@example.com', age: 25 }); await user.save();
Querying Documents
typescript// Query single document const user: IUser | null = await User.findById(userId); // Query multiple documents const users: IUser[] = await User.find({ age: { $gte: 18 } }); // Use lean() to return plain objects const plainUsers = await User.find().lean();
Updating Documents
typescriptconst user: IUser | null = await User.findById(userId); if (user) { user.age = 26; await user.save(); } // Use findOneAndUpdate const updatedUser: IUser | null = await User.findOneAndUpdate( { email: 'john@example.com' }, { age: 26 }, { new: true } );
Static Method Types
Define Static Methods
typescriptinterface IUserModel extends Model<IUser> { findByEmail(email: string): Promise<IUser | null>; findAdults(): Promise<IUser[]>; countByAge(minAge: number): Promise<number>; } // Implement static methods userSchema.statics.findByEmail = function(email: string): Promise<IUser | null> { return this.findOne({ email }); }; userSchema.statics.findAdults = function(): Promise<IUser[]> { return this.find({ age: { $gte: 18 } }); }; userSchema.statics.countByAge = function(minAge: number): Promise<number> { return this.countDocuments({ age: { $gte: minAge } }); }; // Use static methods const user = await User.findByEmail('john@example.com'); const adults = await User.findAdults(); const count = await User.countByAge(18);
Instance Method Types
Define Instance Methods
typescriptinterface IUser extends Document { name: string; email: string; age: number; getFullName(): string; isAdult(): boolean; updateAge(newAge: number): Promise<IUser>; } // Implement instance methods userSchema.methods.getFullName = function(): string { return this.name; }; userSchema.methods.isAdult = function(): boolean { return this.age >= 18; }; userSchema.methods.updateAge = async function(newAge: number): Promise<IUser> { this.age = newAge; return this.save(); }; // Use instance methods const user = await User.findById(userId); if (user) { console.log(user.getFullName()); console.log(user.isAdult()); await user.updateAge(26); }
Virtual Field Types
Define Virtual Fields
typescriptinterface IUser extends Document { firstName: string; lastName: string; fullName: string; // Virtual field } const userSchema: Schema = new Schema({ firstName: { type: String, required: true }, lastName: { type: String, required: true } }); // Define virtual field userSchema.virtual('fullName').get(function(this: IUser): string { return `${this.firstName} ${this.lastName}`; }); userSchema.virtual('fullName').set(function(this: IUser, value: string): void { const parts = value.split(' '); this.firstName = parts[0]; this.lastName = parts[1]; });
Nested Document Types
Define Nested Schema
typescriptinterface IAddress { street: string; city: string; state: string; zipCode: string; } interface IUser extends Document { name: string; email: string; address: IAddress; } const addressSchema: Schema = new Schema({ street: { type: String, required: true }, city: { type: String, required: true }, state: { type: String, required: true }, zipCode: { type: String, required: true } }); const userSchema: Schema = new Schema({ name: { type: String, required: true }, email: { type: String, required: true }, address: { type: addressSchema, required: true } });
Array Field Types
Define Array Types
typescriptinterface IUser extends Document { name: string; tags: string[]; scores: number[]; } const userSchema: Schema = new Schema({ name: { type: String, required: true }, tags: [String], scores: [Number] });
Reference Types
Define References
typescriptinterface IPost extends Document { title: string; content: string; author: mongoose.Types.ObjectId; } interface IUser extends Document { name: string; email: string; posts: mongoose.Types.ObjectId[]; } const postSchema: Schema = new Schema({ title: { type: String, required: true }, content: { type: String, required: true }, author: { type: Schema.Types.ObjectId, ref: 'User' } }); const userSchema: Schema = new Schema({ name: { type: String, required: true }, email: { type: String, required: true }, posts: [{ type: Schema.Types.ObjectId, ref: 'Post' }] }); // Use populate const user = await User.findById(userId).populate('posts');
Middleware Types
Define Middleware
typescriptimport { HookNextFunction } from 'mongoose'; // Pre middleware userSchema.pre('save', function(this: IUser, next: HookNextFunction): void { this.email = this.email.toLowerCase(); next(); }); // Post middleware userSchema.post('save', function(this: IUser, doc: IUser): void { console.log('User saved:', doc.email); }); // Async middleware userSchema.pre('save', async function(this: IUser, next: HookNextFunction): Promise<void> { if (await emailExists(this.email)) { return next(new Error('Email already exists')); } next(); }); async function emailExists(email: string): Promise<boolean> { const count = await User.countDocuments({ email }); return count > 0; }
Generic Models
Create Generic Models
typescriptinterface BaseModel extends Document { createdAt: Date; updatedAt: Date; } function createTimestampedModel<T extends Document>( name: string, schema: Schema ): Model<T & BaseModel> { schema.add({ createdAt: { type: Date, default: Date.now }, updatedAt: { type: Date, default: Date.now } }); schema.pre('save', function(this: T & BaseModel, next: HookNextFunction): void { this.updatedAt = new Date(); next(); }); return mongoose.model<T & BaseModel>(name, schema); } // Use generic model interface IUser extends Document { name: string; email: string; } const userSchema: Schema = new Schema({ name: { type: String, required: true }, email: { type: String, required: true } }); const User = createTimestampedModel<IUser>('User', userSchema);
Best Practices
- Use interfaces to define types: Define interfaces for all schemas
- Strict type checking: Enable TypeScript's strict mode
- Avoid any type: Use specific types as much as possible
- Use type assertions: Use type assertions when necessary
- Complete documentation: Add clear comments to types
- Test coverage: Write tests for typed models
- Use tools: Leverage TypeScript's type hints and autocomplete