
本文详解如何在 typescript 环境中安全、类型兼容地为 mongoose `query` 原型添加 `.cache()` 方法,解决声明合并、泛型不匹配、`arguments` 类型错误及私有属性访问等典型问题。
在 typescript 项目中为 Mongoose 查询链式方法(如 .find())动态注入缓存能力,是提升 I/O 密集型应用性能的常见实践。但直接将 javaScript 版本迁移至 TypeScript 时,常因类型系统严格性而报错——例如 All declarations of ‘Query’ must have identical type parameters、Property ‘mongooseCollection’ does not exist 或 Argument of type ‘IArguments’ is not assignable to ‘[]’。这些问题并非代码逻辑错误,而是 TypeScript 类型声明与运行时行为不一致所致。以下提供经过生产验证的、类型安全的完整实现方案。
✅ 正确的类型声明合并(Declaration Merging)
Mongoose 官方类型定义中,Query 是一个六元泛型接口:
Interface Query
若在 declare module ‘mongoose’ 中仅写 interface Query
declare module 'mongoose' { interface Query< ResultType, DocType, THelpers = {}, RawDocType = DocType, QueryOp = "find" > { useCache?: boolean; hashKey?: string; cache(options?: { key?: unknown }): Query; } }
⚠️ 注意:cache 方法返回类型必须与当前泛型参数完全一致(而非 Query),否则链式调用(如 Model.find().cache().sort())将丢失类型推导。
✅ 修复 exec.apply() 的类型错误
原始 js 中 exec.apply(this, arguments) 在 TS 中报错,根本原因有二:
- exec 方法签名实际为 exec(): promise
(无参数),arguments 类型为 IArguments,与空参数数组 [] 不兼容; - arguments 是非类型安全的类数组对象,TS 推荐显式传参或直接调用。
✅ 正确写法(无需传参):
const exec = Query.prototype.exec; Query.prototype.exec = async function (): Promise { if (!this.useCache) { return exec.call(this); // ✅ 推荐:比 apply 更语义化,且无参数歧义 } // ... 缓存逻辑 const result = await exec.call(this); // ✅ 同上 return result; };
✅ 替代已移除的 this.mongooseCollection
this.mongooseCollection 是 Mongoose 内部属性,未暴露于 TypeScript 类型定义中(尽管 JS 运行时存在)。官方文档与较旧教程(如 2020 年前 redis 缓存示例)曾误用此属性,但现代 Mongoose(v6+)应通过模型获取集合名:
// ❌ 错误:类型不安全,TS 编译失败 // collection: this.mongooseCollection.name // ✅ 正确:类型安全,符合 Mongoose v6+ API collection: this.model.collection.name
this.model.collection.name 是公开、稳定且类型完备的属性,可安全用于缓存键构造。
✅ 完整可运行代码(含类型注解)
import mongoose, { Query } from 'mongoose'; import redisClient from './redisClient'; // ✅ 1. 精确声明合并:复刻 Mongoose Query 六元泛型 declare module 'mongoose' { interface Query< ResultType, DocType, THelpers = {}, RawDocType = DocType, QueryOp = "find" > { useCache?: boolean; hashKey?: string; cache(options?: { key?: unknown }): Query; } } // ✅ 2. 保存原始 exec 方法 const exec = Query.prototype.exec; // ✅ 3. 实现 cache 方法(返回 this,保持链式) Query.prototype.cache = function (options = {}) { this.useCache = true; this.hashKey = json.stringify(options.key || ''); return this; }; // ✅ 4. 重写 exec:类型安全 + 缓存逻辑 Query.prototype.exec = async function (): Promise { if (!this.useCache) { return exec.call(this) as Promise; } // ✅ 构造缓存键:使用 model.collection.name 替代 mongooseCollection const key = JSON.stringify({ ...this.getFilter(), collection: this.model.collection.name, }); const cachedValue = await RedisClient.getHCache(this.hashKey, key); if (cachedValue) { const doc = JSON.parse(cachedValue); // ✅ 保持类型一致性:实例化为当前 model 的文档 return Array.isArray(doc) ? (doc.map(d => new this.model(d)) as unknown as ResultType) : (new this.model(doc) as unknown as ResultType); } // ✅ 执行原始查询 const result = await exec.call(this) as ResultType; await RedisClient.setHCache(this.hashKey, key, JSON.stringify(result)); return result; };
? 使用示例与注意事项
// 在业务代码中直接使用(类型推导完整) const users = await User.find({ active: true }) .cache({ key: 'active_users' }) .sort({ createdAt: -1 }) .limit(10) .exec(); // ✅ 返回 Promise,IDE 可智能提示
关键注意事项:
- 避免 any 泛滥:示例中 as unknown as ResultType 是必要类型断言(因 JSON.parse 失去结构信息),但应确保 RedisClient 存储的数据结构与 Mongoose 文档完全一致;
- 缓存键唯一性:this.getFilter() 仅包含查询条件,需额外加入 collection 名以防止不同 Model 的缓存键冲突;
- 错误处理增强(生产建议):在 getHCache/setHCache 周围添加 try/catch,避免 Redis 故障导致整个查询失败;
- 内存泄漏风险:长期运行需监控 useCache 属性是否被意外持久化,建议在 exec 结束后重置 this.useCache = false(可选)。
通过以上改造,你获得的不仅是一个可用的缓存插件,更是一个类型严谨、可维护、与 Mongoose 主版本演进兼容的 TypeScript 扩展方案。