如何在Mongoose回调函数中正确推断TypeScript类型?
问题背景
将Node.js项目迁移至TypeScript v4.9.4时,搭配Mongoose v6.9.0遇到类型推断异常:
- 调用
findOne()等查询方法的回调函数中,document参数的类型无法自动识别,即使显式指定为对应模型类型,也无法读取实例方法 - 但调用带回调的
create()方法时,类型推断完全正常 - 按照Mongoose官方文档「Schemas: Instance Methods」「Statics and Methods in TypeScript」配置Schema后,问题依然存在
复现代码
user.model.ts
import { HydratedDocument, InferSchemaType, Schema, model } from "mongoose"; export type User = HydratedDocument<InferSchemaType<typeof userSchema>>; const userSchema = new Schema({ email: { type: String, unique: true, required: true, }, name: { type: String, required: true, }, hash: String, salt: String, }, { methods: { setPassword(password: string) { // 盐值与哈希逻辑 }, }, toJSON: { virtuals: true }, toObject: { virtuals: true } }); export const UserModel = model("User", userSchema);
auth.service.ts
import { CallbackError } from "mongoose"; import { User, UserModel } from "../models/user.model"; export function login(username, password) { UserModel.findOne({ email: username }, function (err: CallbackError, user: User) { // 报错:Property 'validPassword' does not exist on type ... if (!user.validPassword(password)) { // 处理密码错误 } }); }
完整错误信息
Property 'validPassword' does not exist on type 'Document<unknown, any, { email: string; name: string; hash?: string | undefined; salt?: string | undefined; }> & { email: string; name: string; hash?: string | undefined; salt?: string | undefined; } & { ...; }'.
原因分析
Mongoose v6.x中,InferSchemaType生成的类型仅包含Schema定义的字段,不会自动关联实例方法;同时回调式查询方法的类型推导逻辑与create()存在差异,导致实例方法无法被TypeScript识别。
解决方案
方案1:手动定义接口关联实例方法
放弃InferSchemaType,先定义包含实例方法的接口,再让Schema和模型关联该接口,确保类型完整:
修改user.model.ts:
import { HydratedDocument, Schema, model, Model } from "mongoose"; // 定义包含字段和实例方法的核心接口 interface IUser { email: string; name: string; hash?: string; salt?: string; setPassword(password: string): void; validPassword(password: string): boolean; // 明确声明实例方法 } // 关联模型与文档类型的接口 interface UserModel extends Model<IUser> {} const userSchema = new Schema<IUser, UserModel>({ email: { type: String, unique: true, required: true, }, name: { type: String, required: true, }, hash: String, salt: String, }, { toJSON: { virtuals: true }, toObject: { virtuals: true } }); // 挂载实例方法 userSchema.methods.setPassword = function(password: string) { // 你的实现逻辑 }; userSchema.methods.validPassword = function(password: string): boolean { // 你的密码验证逻辑 return true; }; export type User = HydratedDocument<IUser>; export const UserModel = model<IUser, UserModel>("User", userSchema);
修改后auth.service.ts中,user的类型会自动包含实例方法:
import { CallbackError } from "mongoose"; import { User, UserModel } from "../models/user.model"; export function login(username: string, password: string) { UserModel.findOne({ email: username }, function (err: CallbackError, user: User) { if (user && !user.validPassword(password)) { // 正常处理逻辑 } }); }
方案2:改用async/await替代回调
Mongoose支持Promise风格调用,改用async/await可直接规避回调的类型推断问题,同时代码更简洁:
export async function login(username: string, password: string) { try { const user = await UserModel.findOne({ email: username }); if (user && !user.validPassword(password)) { // 处理密码错误 } } catch (err) { // 处理查询异常 } }
这种方式下,TypeScript能自动正确推断user的完整类型,包括所有实例方法。
补充说明
你提到的将实例方法抽离到单独服务模块是可行的替代方案,但如果希望保留Mongoose实例方法的写法,上述两种方案更贴合Mongoose的TypeScript最佳实践。
内容的提问来源于stack exchange,提问作者exobiotic

