5月28日 00:11

Mongoose 如何与 TypeScript 结合使用?

Mongoose 与 TypeScript 结合的核心在于:通过接口定义文档类型,用泛型参数绑定 Schema 与 Model,让编译器在写查询、操作文档时提供类型检查和自动补全。Mongoose v7+ 推荐使用 HydratedDocument 替代 extends Document,并支持从 Schema 定义自动推断文档类型。

核心模式:Schema 泛型 + HydratedDocument

Mongoose v7+ 推荐的写法不再让接口继承 Document,而是通过 Schema 泛型参数让 Mongoose 自动推断文档类型,再用 HydratedDocument 包装获得完整实例类型:

typescript
import mongoose, { Schema, HydratedDocument, Model } from 'mongoose'; interface IUser { name: string; email: string; age: number; createdAt: Date; } interface IUserMethods { isAdult(): boolean; } type UserModel = Model<IUser, {}, IUserMethods>; const userSchema = new Schema<IUser, UserModel, IUserMethods>({ name: { type: String, required: true }, email: { type: String, required: true, unique: true }, age: { type: Number, min: 0 }, createdAt: { type: Date, default: Date.now } }); userSchema.method('isAdult', function(this: HydratedDocument<IUser & IUserMethods>) { return this.age >= 18; }); type UserDoc = HydratedDocument<IUser, IUserMethods>; const User = mongoose.model<IUser, UserModel>('User', userSchema);

关键变化:IUser 是纯数据接口,不继承 Document;Schema 的三个泛型参数分别为 RawDocTypeModelTypeInstanceMethods;HydratedDocument 将纯接口转为含 Mongoose 方法的完整文档类型。

静态方法与查询助手

静态方法定义在扩展 Model 的接口中,查询助手通过 Schema 第四个泛型参数声明:

typescript
interface IUserModel extends Model<IUser, {}, IUserMethods, IUserQueryHelpers> { findByEmail(email: string): Promise<UserDoc | null>; } interface IUserQueryHelpers { byAge(min: number): Query<IUser[], IUser, IUserQueryHelpers>; } const userSchema = new Schema<IUser, IUserModel, IUserMethods, IUserQueryHelpers>({ // ...字段定义 }); userSchema.static('findByEmail', function(email: string) { return this.findOne({ email }); }); userSchema.query.byAge = function(min: number) { return this.where('age').gte(min); }; // 链式调用有类型提示 const adults = await User.find().byAge(18);

虚拟字段与中间件

虚拟字段需要在 Schema 的泛型参数或 virtual 选项中声明才能获得类型推断。中间件的 this 指向 HydratedDocument,无需再导入已废弃的 HookNextFunction:

typescript
userSchema.virtual('displayName').get(function(this: UserDoc) { return `${this.name} <${this.email}>`; }); userSchema.pre('save', function(this: UserDoc) { this.email = this.email.toLowerCase(); });

关联与 populate 的类型安全

ref 字段用 ObjectId 声明,populate 时通过泛型参数指定填充后的文档类型:

typescript
interface IPost { title: string; author: mongoose.Types.ObjectId; } const postSchema = new Schema<IPost>({ title: { type: String, required: true }, author: { type: Schema.Types.ObjectId, ref: 'User' } }); // populate 返回值需手动标注或使用类型工具 const post = await Post.findById(id).populate('author');

Mongoose 无法自动推断 populate 后的字段类型,需手动用类型交叉处理,这是目前社区常见的痛点。

lean() 的类型处理

.lean() 返回纯 JavaScript 对象而非 Mongoose Document,类型应使用 FlattenMaps 或手动定义原始类型:

typescript
type LeanUser = mongoose.FlattenMaps<IUser>; const users: LeanUser[] = await User.find().lean();

追问

  1. 为什么 Mongoose v7+ 不再推荐 interface IUser extends Document?extends Document 会把 Mongoose 内部属性(如 $__、$isNew)混入业务接口,导致类型污染;HydratedDocument 通过包装层隔离,接口保持纯净。

  2. Schema 的 9 个泛型参数分别是什么?依次为 RawDocType、TModelType、TInstanceMethods、TQueryHelpers、TVirtuals、TStaticMethods、TSchemaOptions、THydratedDocumentType、TPathTypeMap,实际开发中通常只填前 3-4 个。

  3. Typegoose 解决了什么问题?Typegoose 用 class + decorator 定义 Schema,一个类同时描述接口和 Schema,避免接口与 Schema 字段重复声明,但引入了装饰器实验特性的依赖。

  4. populate 后如何获得完整类型?可定义一个带 author 详情的联合类型,或使用 Mongoose 的 Promise<HydratedDocument<IPost & { author: UserDoc }>> 手动标注。

  5. 子文档数组的类型如何定义?使用 Types.DocumentArray<ISubDoc> 声明,它在 HydratedDocument 中会自动映射为带 Mongoose 子文档方法的数组类型。

标签:Mongoose