TypeScript 中的模式

Mongoose 模式 是你告诉 Mongoose 你的文档结构的方式。Mongoose 模式与 TypeScript 接口是分开的,因此你需要定义一个原始文档接口和一个模式;或者依赖 Mongoose 自动从模式定义中推断类型。

自动类型推断

Mongoose 可以根据你的模式定义自动推断文档类型,如下所示。我们建议在定义模式和模型时依赖自动类型推断。

import { Schema, model } from 'mongoose';
// Schema
const schema = new Schema({
  name: { type: String, required: true },
  email: { type: String, required: true },
  avatar: String
});

// `UserModel` will have `name: string`, etc.
const UserModel = mongoose.model('User', schema);

const doc = new UserModel({ name: 'test', email: 'test' });
doc.name; // string
doc.email; // string
doc.avatar; // string | undefined | null

使用自动类型推断有一些注意事项

  1. 你需要在 tsconfig.json 中设置 strictNullChecks: truestrict: true。或者,如果你在命令行中设置标志,则为 --strictNullChecks--strict。在禁用严格模式的情况下,自动类型推断存在 已知问题
  2. 你需要在 new Schema() 调用中定义你的模式。不要将你的模式定义分配给一个临时变量。执行类似 const schemaDefinition = { name: String }; const schema = new Schema(schemaDefinition); 的操作将不起作用。
  3. Mongoose 会将 createdAtupdatedAt 添加到你的模式中,前提是你已经在模式中指定了 timestamps 选项,除非你也指定了 methodsvirtualsstatics。在使用时间戳和 methods/virtuals/statics 选项时,类型推断存在 已知问题。如果你使用 methods、virtuals 和 statics,则需要负责将 createdAtupdatedAt 添加到你的模式定义中。

如果你必须单独定义模式,请使用 as const (const schemaDefinition = { ... } as const;) 来防止类型扩展。TypeScript 会自动将 required: false 等类型扩展为 required: boolean,这会导致 Mongoose 假设该字段是必需的。使用 as const 会强制 TypeScript 保留这些类型。

如果你需要从模式定义中显式获取原始文档类型(从 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 = InferRawDocType<typeof schemaDefinition>;

useRawDoc(doc.toObject());

function useRawDoc(doc: RawUserDocument) {
  // ...
}

如果自动类型推断不适合你,你始终可以回退到文档接口定义。

单独的文档接口定义

如果自动类型推断不适合你,你可以定义一个单独的原始文档接口,如下所示。

import { Schema } from 'mongoose';

// Raw document interface. Contains the data type as it will be stored
// in MongoDB. So you can ObjectId, Buffer, and other custom primitive data types.
// But no Mongoose document arrays or subdocuments.
interface User {
  name: string;
  email: string;
  avatar?: string;
}

// Schema
const schema = new Schema<User>({
  name: { type: String, required: true },
  email: { type: String, required: true },
  avatar: String
});

默认情况下,Mongoose 不会检查你的原始文档接口是否与你的模式一致。例如,上面的代码如果在文档接口中 email 是可选的,但在 schema 中是 required 的,不会抛出错误。

泛型参数

TypeScript 中的 Mongoose Schema 类有 9 个 泛型参数

  • RawDocType - 描述数据如何在 MongoDB 中保存的接口
  • TModelType - Mongoose 模型类型。如果没有要定义的查询助手或实例方法,则可以省略。
    • 默认值:Model<DocType, any, any>
  • TInstanceMethods - 包含模式方法的接口。
    • 默认值:{}
  • TQueryHelpers - 包含在模式上定义的查询助手的接口。默认值为 {}
  • TVirtuals - 包含在模式上定义的虚拟属性的接口。默认值为 {}
  • TStaticMethods - 包含模型上方法的接口。默认值为 {}
  • TSchemaOptions - 传递给 Schema() 构造函数的第二个选项的类型。默认值为 DefaultSchemaOptions
  • DocType - 从模式中推断的文档类型。
  • THydratedDocumentType - 已水化的文档类型。这是 await Model.findOne()Model.hydrate() 等的默认返回值类型。
查看 TypeScript 定义
export class Schema<
  RawDocType = any,
  TModelType = Model<RawDocType, any, any, any>,
  TInstanceMethods = {},
  TQueryHelpers = {},
  TVirtuals = {},
  TStaticMethods = {},
  TSchemaOptions = DefaultSchemaOptions,
  DocType = ...,
  THydratedDocumentType = HydratedDocument<FlatRecord<DocType>, TVirtuals & TInstanceMethods>
>
  extends events.EventEmitter {
  // ...
}

第一个泛型参数 DocType 表示 Mongoose 将存储在 MongoDB 中的文档类型。Mongoose 会将 DocType 包装在一个 Mongoose 文档中,用于诸如文档中间件的 this 参数等情况。例如

schema.pre('save', function(): void {
  console.log(this.name); // TypeScript knows that `this` is a `mongoose.Document & User` by default
});

第二个泛型参数 M 是与模式一起使用的模型。Mongoose 在模式中定义的模型中间件中使用 M 类型。

第三个泛型参数 TInstanceMethods 用于添加在模式中定义的实例方法的类型。

第四个参数 TQueryHelpers 用于添加 链式查询助手 的类型。

模式与接口字段

Mongoose 会检查以确保模式中的每个路径都在文档接口中定义。

例如,以下代码将无法编译,因为 email 是模式中的一个路径,但在 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 Schema<User>({
  name: { type: String, required: true },
  emaill: { type: String, required: true },
  avatar: String
});

但是,Mongoose 不会检查文档接口中存在但在模式中不存在的路径。例如,以下代码可以编译。

import { Schema, Model } from 'mongoose';

interface User {
  name: string;
  email: string;
  avatar?: string;
  createdAt: number;
}

const schema = new Schema<User, Model<User>>({
  name: { type: String, required: true },
  email: { type: String, required: true },
  avatar: String
});

这是因为 Mongoose 有许多功能可以将路径添加到你的模式中,这些路径应该包含在 DocType 接口中,而无需你显式地将这些路径放在 Schema() 构造函数中。例如,时间戳插件

数组

当你在文档接口中定义数组时,我们建议使用普通 JavaScript 数组,不要使用 Mongoose 的 Types.Array 类型或 Types.DocumentArray 类型。相反,使用 THydratedDocumentType 泛型来定义模型和模式,以指示已水化的文档类型具有 Types.ArrayTypes.DocumentArray 类型的路径。

import mongoose from 'mongoose'
const { Schema } = mongoose;

interface IOrder {
  tags: Array<{ name: string }>
}

// Define a HydratedDocumentType that describes what type Mongoose should use
// for fully hydrated docs returned from `findOne()`, etc.
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.model<IOrder, OrderModelType>('Order', orderSchema);

// Demonstrating return types from 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 }>
};

对于数组子文档类型,使用 HydratedArraySubdocument<RawDocType>,对于单个子文档,使用 HydratedSingleSubdocument<RawDocType>

如果你没有使用 模式方法、中间件或 虚拟属性,则可以省略 Schema() 的最后 7 个泛型参数,只需使用 new mongoose.Schema<IOrder, OrderModelType>(...) 定义你的模式。模式的 THydratedDocumentType 参数主要用于设置方法和虚拟属性的 this 的值。