如何用TypeScript实现支持自动补全的Mongoose接口生成器?
TypeScript实现Mongoose自动补全生成器方案
核心思路
通过泛型追踪实体类型,结合运行时动态挂载模型,让TypeScript在编译时自动推断属性类型,实现完整的自动补全支持。同时解决你之前使用Object.defineProperty未成功的问题,无需手动维护声明文件。
完整实现代码
import mongoose, { Schema, Model, Document } from 'mongoose'; // 类型工具:将Mongoose字段定义转换为TypeScript文档类型 type MongooseFieldToTs<T> = T extends typeof String ? string : T extends typeof Number ? number : T extends typeof Boolean ? boolean : T extends typeof Date ? Date : T extends Array<infer U> ? MongooseFieldToTs<U>[] : T extends { type: infer U } ? MongooseFieldToTs<U> : any; // 将实体字段映射为文档类型 type EntityToDocument<T extends Record<string, any>> = { [K in keyof T]: MongooseFieldToTs<T[K]>; }; class Database<T extends Record<string, any> = {}> { protected entities = new Map<string, Record<string, any>>(); public models: Partial<{ [K in keyof T]: Model<Document & EntityToDocument<T[K]>> }> = {}; /** * 注册实体 * @param param 实体名称与字段定义 * @returns 扩展了实体类型的Database实例 */ public entity<K extends string, F extends Record<string, any>>( { name, fields }: { name: K; fields: F } ): Database<T & { [key in K]: F }> { this.entities.set(name, fields); return this as Database<T & { [key in K]: F }>; } /** * 生成Mongoose模型并挂载到实例 * @returns 包含所有模型的Database实例(自动补全生效) */ public generator(): Omit<this, 'entity' | 'generator'> & { [K in keyof T]: Model<Document & EntityToDocument<T[K]>> } { // 遍历注册的实体,创建Schema与Model this.entities.forEach((fields, entityName) => { const schema = new Schema(fields); const model = mongoose.model(entityName, schema); // 挂载到models对象 this.models[entityName as keyof T] = model; // 直接挂载到Database实例 Object.defineProperty(this, entityName, { get: () => model, enumerable: true, configurable: true }); }); // 类型断言让TypeScript识别动态添加的属性 return this as Omit<this, 'entity' | 'generator'> & { [K in keyof T]: Model<Document & EntityToDocument<T[K]>> }; } } // 使用示例 (async () => { // 连接MongoDB await mongoose.connect('mongodb://localhost:27017/test'); // 注册实体并生成模型 const db = new Database() .entity({ name: 'user', fields: { name: String, age: Number, isActive: Boolean, createdAt: Date } }) .entity({ name: 'post', fields: { title: String, content: String, author: { type: Schema.Types.ObjectId, ref: 'user' }, tags: [String] } }) .generator(); // 自动补全生效:db.user、db.models.user都能提示Model的方法与文档属性 const newUser = await db.user.create({ name: 'Alice', age: 25, isActive: true, createdAt: new Date() }); const posts = await db.models.post.find({ author: newUser._id }); })();
关键细节说明
泛型类型追踪:
Database类使用泛型T存储所有已注册的实体类型,每次调用entity时,泛型会自动扩展,添加新的实体名称与字段类型。EntityToDocument类型工具将Mongoose的字段定义(如String、Number)转换为对应的TypeScript类型(string、number),让文档属性也能获得自动补全。
运行时模型挂载:
- 通过
Object.defineProperty将生成的Mongoose模型直接挂载到Database实例,同时也挂载到models对象,满足两种使用方式。 - 配置
enumerable: true确保属性能被遍历到,解决你之前可能遇到的属性不可见问题。
- 通过
自动补全实现:
generator方法返回的类型移除了entity和generator方法(生成后无需再调用),并添加了所有实体对应的模型属性,TypeScript编译时能直接推断出这些属性的存在,从而提供自动补全。
可选:生成单独声明文件
如果你仍需要生成静态声明文件,可以添加以下脚本,在generator方法中调用即可:
import fs from 'fs'; function generateDeclarationFile(entities: Map<string, Record<string, any>>) { let content = `import mongoose, { Model, Document } from 'mongoose';\n\n`; // 定义每个实体的Document接口 entities.forEach((fields, name) => { const docInterfaceName = `${name.charAt(0).toUpperCase()}${name.slice(1)}Document`; content += `export interface ${docInterfaceName} {\n`; Object.entries(fields).forEach(([key, type]) => { const tsType = convertMongooseTypeToTs(type); content += ` ${key}: ${tsType};\n`; }); content += `}\n\n`; }); // 定义模型映射接口 content += `export interface DatabaseModels {\n`; entities.forEach((_, name) => { const docInterfaceName = `${name.charAt(0).toUpperCase()}${name.slice(1)}Document`; content += ` ${name}: Model<Document & ${docInterfaceName}>;\n`; }); content += `}\n\n`; // 定义生成后的Database类类型 content += `export declare class GeneratedDatabase {\n`; content += ` public models: DatabaseModels;\n`; entities.forEach((_, name) => { const docInterfaceName = `${name.charAt(0).toUpperCase()}${name.slice(1)}Document`; content += ` public ${name}: Model<Document & ${docInterfaceName}>;\n`; }); content += `}\n`; fs.writeFileSync('./database.d.ts', content); } function convertMongooseTypeToTs(mongooseType: any): string { if (mongooseType === String) return 'string'; if (mongooseType === Number) return 'number'; if (mongooseType === Boolean) return 'boolean'; if (mongooseType === Date) return 'Date'; if (Array.isArray(mongooseType)) return `${convertMongooseTypeToTs(mongooseType[0])}[]`; if (typeof mongooseType === 'object' && mongooseType.type) { if (mongooseType.type === Schema.Types.ObjectId) return 'mongoose.Types.ObjectId'; return convertMongooseTypeToTs(mongooseType.type); } return 'any'; } // 在Database的generator方法中添加: // generateDeclarationFile(this.entities);
内容的提问来源于stack exchange,提问作者Speek
相关产品推荐
相关产品推荐

