Mongoose TypeScript 指南:从 Schema 定义到类型安全文档模型
Mongoose TypeScript 指南从 Schema 定义到类型安全文档模型【免费下载链接】mongooseMongoDB object modeling designed to work in an asynchronous environment.项目地址: https://gitcode.com/GitHub_Trending/mo/mongooseMongoose 的 schema 用于描述文档在 MongoDB 中的结构它与 TypeScript 的 interface 是两套独立的体系。本指南围绕 docs/typescript/schemas.md 讲解如何在 Mongoose 中写出类型安全的 schema既可以选择让 Mongoose 从 schema 定义自动推断文档类型也可以手写 raw document interface 作为兜底方案同时深入剖析Schema类的泛型参数、字段一致性检查规则以及数组/子文档在类型系统中的正确建模方式。读完本文你将能够在当前仓库mo/mongoose中为任意模型搭建可编译、可维护、与运行时行为一致的 TypeScript 类型体系。自动类型推断推荐的首选方案Mongoose 可以自动从 schema 定义中推断出文档类型无需手写任何 interface。官方文档明确推荐在定义 schema 与 model 时优先依赖自动类型推断import { Schema, model } from mongoose; // Schema const schema new Schema({ name: { type: String, required: true }, email: { type: String, required: true }, avatar: String }); // UserModel 会自动获得 name: string、email: string 等字段类型 const UserModel model(User, schema); const doc new UserModel({ name: test, email: test }); doc.name; // string doc.email; // string doc.avatar; // string | undefined | null从源码结构看这套推断能力并非魔法在 types/index.d.ts 中Schema类的第 8 个泛型参数DocType默认通过ObtainDocumentTypeany, RawDocType, ...从 schema 定义中推导而 types/inferschematype.d.ts 中的ObtainDocumentType会逐字段判断路径是否为required、是否允许null从而生成精确的字段类型例如required: true的字段推断为必选String直接简写的字段推断为可选并允许null。自动推断的三个前置条件使用自动类型推断时有以下三个注意事项必须开启严格空值检查需要在tsconfig.json中设置strictNullChecks: true或strict: true如果使用命令行编译则传入--strictNullChecks或--strict。官方文档指出关闭 strict 模式时自动类型推断存在已知问题 中IsPathRequired与 types/inferschematype.d.ts 中PathAllowsNull的判定分支。schema 定义必须内联在new Schema()调用中不要先把定义赋给临时变量再传入。像const schemaDefinition { name: String }; const schema new Schema(schemaDefinition);这样的写法无法触发自动推断——因为类型推断依赖于字面量对象在Schema构造函数泛型参数中的直接捕获。timestamps选项会注入额外字段只要在 schema 中开启了timestamps选项Mongoose 就会自动向推断出的类型中加入createdAt与updatedAt字段。其类型逻辑实现在 types/inferschematype.d.ts 的DefaultTimestampPropscreatedAt: NativeDate、updatedAt: NativeDate与ResolveTimestamps中。必须拆分定义时用as const防止类型拓宽如果出于可读性或复用考虑必须把 schema 定义拆分出来请使用as const断言const schemaDefinition { name: { type: String, required: true }, email: { type: String, required: true }, avatar: String } as const; const schema new Schema(schemaDefinition);原因在于 TypeScript 会自动发生类型拓宽type widening例如required: false会被拓宽成required: boolean从而让 Mongoose 误以为该字段是必填的。as const能强制 TypeScript 保留这些字面量类型确保IsPathRequired见 types/inferschematype.d.ts能精确识别每个字段的必填性。显式获取原始文档类型InferRawDocType当你需要显式取出原始文档类型即doc.toObject()、await Model.findOne().lean()等返回值的类型时可以使用 Mongoose 的InferRawDocType辅助类型import { Schema, InferRawDocType, model } from mongoose; const schemaDefinition { name: { type: String, required: true }, email: { type: String, required: true }, avatar: String } as const; const schema new Schema(schemaDefinition); const UserModel model(User, schema); const doc new UserModel({ name: test, email: test }); type RawUserDocument InferRawDocTypetypeof schemaDefinition; useRawDoc(doc.toObject()); function useRawDoc(doc: RawUserDocument) { // ... }InferRawDocType的定义位于 types/inferrawdoctype.d.ts它会遍历 schema 定义中的每个路径把子文档/文档数组转换成普通 POJO 结构并默认附加_id除非设置了_id: false。与之配套的还有InferRawDocTypeFromSchema接收一个typeof schema实例而非定义对象其实现见 types/inferrawdoctype.d.ts。关于 lean 查询与原始类型的更多组合用法可参考 test/types/schema.test.ts 中的类型测试用例。独立文档接口定义兜底方案如果自动类型推断不适用例如项目要求接口与 schema 分离、或者历史代码结构不允许内联定义可以显式声明 raw document interfaceimport { Schema } from mongoose; // 原始文档接口描述数据在 MongoDB 中的存储形态。 // 可以使用 ObjectId、Buffer 等自定义原始类型 // 但不包含 Mongoose 的文档数组DocumentArray与子文档Subdocument。 interface User { name: string; email: string; avatar?: string; } // Schema const schema new SchemaUser({ name: { type: String, required: true }, email: { type: String, required: true }, avatar: String });需要特别注意的是默认情况下 Mongoose 不会检查 raw document interface 与 schema 是否对齐。例如上面的代码中即使 interface 里email是可选的、而 schema 里email是required: true编译也不会报错。这种接口定义的是存储形态、schema 负责运行时校验的职责分离需要开发者自行保持二者一致。从类型实现上看Schema的第一个泛型参数RawDocType表示数据在 MongoDB 中如何保存而真正的校验逻辑仍由 schema 的运行时定义负责二者在类型层面并不强绑定。Schema 泛型参数详解Mongoose 的Schema类在 TypeScript 中拥有 9 个泛型参数官方文档表述从当前仓库 types/index.d.ts 的实际源码看Schema类的泛型声明为 11 个多出的TSchemaDefinition与LeanResultType是内部推断辅助参数。它们依次是参数含义默认值RawDocType描述数据如何保存到 MongoDB 的接口anyTModelType文档中也写作M与该 schema 绑定的 Mongoose model 类型没有查询助手或实例方法时可省略ModelRawDocType, any, any, anyTInstanceMethods定义在 schema 上的实例方法接口{}TQueryHelpers定义在 schema 上的可链式查询助手接口{}TVirtuals定义在 schema 上的虚拟属性接口{}TStaticMethods定义在 model 上的静态方法接口{}TSchemaOptions传给Schema()构造函数的第二个参数类型DefaultSchemaOptionsDocType从 schema 推断出的文档类型由ObtainDocumentType自动推导THydratedDocumentType水合文档类型即await Model.findOne()、Model.hydrate()等的默认返回类型HydratedDocumentFlatRecordDocType, TVirtuals TInstanceMethods源码中的类签名如下摘自 types/index.d.tsexport class Schema RawDocType any, TModelType ModelRawDocType, any, any, any, TInstanceMethods {}, TQueryHelpers {}, TVirtuals {}, TStaticMethods {}, TSchemaOptions DefaultSchemaOptions, DocType extends ApplySchemaOptions... ..., THydratedDocumentType HydratedDocument..., TSchemaDefinition ..., LeanResultType ... extends events.EventEmitter { // ... }各泛型参数的实际用途如下DocType第一个泛型参数文档中此处的描述与源码中第 8 个位置对应表示 Mongoose 将存入 MongoDB 的文档类型。Mongoose 会把它包装进 Mongoose document典型场景是文档中间件中的this参数。例如schema.pre(save, function(): void { console.log(this.name); // TypeScript 知道 this 是 mongoose.Document User });M第二个泛型参数即TModelType表示与 schema 配合使用的 model 类型。Mongoose 在 schema 中定义的 model 中间件里使用M类型。TInstanceMethods第三个泛型参数为 schema 中定义的实例方法补充类型。TQueryHelpers第四个泛型参数为可链式查询助手补充类型具体用法参见 docs/typescript/query-helpers.md。少写泛型参数的快捷方式如果既没有 schema methods参见 docs/guide.md 中的methods一节、也没有中间件或 virtuals参见 docs/tutorials/virtuals.md则可以省略Schema()的后面 7 个泛型参数只写new mongoose.SchemaIOrder, OrderModelType(...)THydratedDocumentType参数对 schema 而言主要用于设置方法methods与虚拟属性virtuals中this的类型。更优的写法Schema.create()在较新的版本中仓库还提供了Schema.create()静态方法它等价于new Schema(definition, options)但拥有更好的自动类型推断——详见 types/index.d.ts 中两个重载的实现它直接从TSchemaDefinition推导RawDocType与THydratedDocumentType并自动把options.methods、options.query、options.virtuals、options.statics中定义的方法/助手/虚拟属性注入到泛型参数中从而避免手写冗长的泛型参数列表。Schema 与接口字段的一致性检查规则Mongoose 会单向检查字段一致性schema 中的每个路径必须存在于文档接口中。例如下面这段代码会编译失败因为emaill拼写错误是 schema 中的路径却不存在于DocType接口import { Schema, Model } from mongoose; interface User { name: string; email: string; avatar?: string; } // 编译报错Object literal may only specify known properties, // but emaill does not exist in type ... // Did you mean to write email? const schema new SchemaUser({ name: { type: String, required: true }, emaill: { type: String, required: true }, avatar: String });文档接口中存在、但 schema 中不存在的路径不会被检查。下面这段代码可以正常编译import { Schema, Model } from mongoose; interface User { name: string; email: string; avatar?: string; createdAt: number; } const schema new SchemaUser, ModelUser({ name: { type: String, required: true }, email: { type: String, required: true }, avatar: String });这种不对称是刻意设计的Mongoose 有大量特性会向 schema 追加路径这些路径理应包含在DocType接口中却不需要显式出现在Schema()构造函数里。典型例子就是 timestamps自动添加createdAt/updatedAt其类型注入逻辑见 types/inferschematype.d.ts以及插件plugins参见 docs/plugins.md。数组与子文档的类型建模在文档接口中定义数组时官方推荐使用原生 JavaScript 数组而不是 Mongoose 的Types.Array或Types.DocumentArray类型。水合文档中数组路径的类型Types.Array、Types.DocumentArray应通过THydratedDocumentType泛型来定义。下面是一个完整的订单示例展示了从接口、水合文档类型、model 类型到 schema 定义的完整链条import mongoose from mongoose; const { Schema } mongoose; interface IOrder { tags: Array{ name: string }; } // 定义 HydratedDocumentType描述从 findOne() 等查询返回的 // 完全水合文档应该具有的类型 type OrderHydratedDocument mongoose.HydratedDocument IOrder, { tags: mongoose.HydratedArraySubdocument{ name: string } } ; type OrderModelType mongoose.Model IOrder, {}, {}, {}, OrderHydratedDocument // THydratedDocumentType ; const orderSchema new mongoose.Schema IOrder, OrderModelType, {}, // methods {}, // query helpers {}, // virtuals {}, // statics mongoose.DefaultSchemaOptions, // schema options IOrder, // doctype OrderHydratedDocument // THydratedDocumentType ({ tags: [{ name: { type: String, required: true } }] }); const OrderModel mongoose.modelIOrder, OrderModelType(Order, orderSchema); // 观察 OrderModel 的返回类型 const doc new OrderModel({ tags: [{ name: test }] }); doc.tags; // mongoose.Types.DocumentArray{ name: string } doc.toObject().tags; // Array{ name: string } async function run() { const docFromDb await OrderModel.findOne().orFail(); docFromDb.tags; // mongoose.Types.DocumentArray{ name: string } const leanDoc await OrderModel.findOne().orFail().lean(); leanDoc.tags; // Array{ name: string } }使用规则总结数组子文档使用HydratedArraySubdocumentRawDocType作为类型单个子文档使用HydratedSingleSubdocumentRawDocType作为类型水合文档中数组路径为Types.DocumentArray...而lean 查询toObject()中数组路径为原生Array...。这一转换逻辑由 types/inferhydrateddoctype.d.ts 中的InferHydratedDocType类型在编译期完成保证两种查询形态下的类型互不混淆。关于HydratedDocument与HydratedArraySubdocument的完整定义可查阅 types/document.d.ts 与 types/types.d.ts在 test/types/schema.test.ts 中还包含了大量针对数组、子文档、枚举、discriminator 等场景的编译期类型断言可作为理解这套类型系统的活教材。总结在 mo/mongoose 中实践 TypeScript 类型安全 schema 的核心路径是优先自动推断把 schema 定义内联在new Schema()中开启strictNullChecks让 Mongoose 借助 types/inferschematype.d.ts 的ObtainDocumentType/InferSchemaType自动推导文档类型必要时手动声明使用new SchemaUser({...})传入RawDocType并记住 Mongoose 只做schema 路径必须存在于接口的单向检查需要分离定义时加as const需要原始文档类型时用InferRawDocType见 types/inferrawdoctype.d.ts数组与子文档使用原生数组类型配合THydratedDocumentType泛型区分水合形态与 lean 形态的返回类型掌握Schema的泛型参数顺序RawDocType、TModelType、TInstanceMethods、TQueryHelpers、TVirtuals、TStaticMethods、TSchemaOptions、DocType、THydratedDocumentType以及Schema.create()这个能自动注入 methods/virtuals/statics 类型的更优入口见 types/index.d.ts。整套类型系统的实现在 types/ 目录下集中维护任何一处推断行为都能在对应的.d.ts与 test/types/ 的类型测试中找到依据。【免费下载链接】mongooseMongoDB object modeling designed to work in an asynchronous environment.项目地址: https://gitcode.com/GitHub_Trending/mo/mongoose创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考