TypeScript 中的结构

¥Schemas in TypeScript

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

¥Mongoose schemas are how you tell Mongoose what your documents look like. Mongoose schemas are separate from TypeScript interfaces, so you need to either define both a raw document interface and a schema; or rely on Mongoose to automatically infer the type from the schema definition.

自动类型推断

¥Automatic type inference

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

¥Mongoose can automatically infer the document type from your schema definition as follows. We recommend relying on automatic type inference when defining schemas and models.

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

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

¥There are a few caveats for using automatic type inference:

  1. 你需要在 tsconfig.json 中设置 strictNullChecks: truestrict: true。或者,如果你在命令行设置标志,则为 --strictNullChecks--strict。有 已知的问题 具有禁用严格结构的自动类型推断。

    ¥You need to set strictNullChecks: true or strict: true in your tsconfig.json. Or, if you're setting flags at the command line, --strictNullChecks or --strict. There are known issues with automatic type inference with strict mode disabled.

  2. 你需要在 new Schema() 调用中定义你的结构。不要将结构定义分配给临时变量。做 const schemaDefinition = { name: String }; const schema = new Schema(schemaDefinition); 之类的事情是行不通的。

    ¥You need to define your schema in the new Schema() call. Don't assign your schema definition to a temporary variable. Doing something like const schemaDefinition = { name: String }; const schema = new Schema(schemaDefinition); will not work.

  3. 如果你在结构中指定 timestamps 选项,Mongoose 会将 createdAtupdatedAt 添加到你的结构中,除非你还指定了 methodsvirtualsstatics。有一个带有时间戳和方法/虚拟/静态选项的类型推断的 已知问题。如果你使用方法、虚函数和静态函数,则你有责任将 createdAtupdatedAt 添加到你的结构定义中。

    ¥Mongoose adds createdAt and updatedAt to your schema if you specify the timestamps option in your schema, except if you also specify methods, virtuals, or statics. There is a known issue with type inference with timestamps and methods/virtuals/statics options. If you use methods, virtuals, and statics, you're responsible for adding createdAt and updatedAt to your schema definition.

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

¥If you must define your schema separately, use as const (const schemaDefinition = { ... } as const;) to prevent type widening. TypeScript will automatically widen types like required: false to required: boolean, which will cause Mongoose to assume the field is required. Using as const forces TypeScript to retain these types.

如果你需要从架构定义中明确获取原始文档类型(从 doc.toObject()await Model.findOne().lean() 等返回的值),你可以使用 Mongoose 的 inferRawDocType 助手,如下所示:

¥If you need to explicitly get the raw document type (the value returned from doc.toObject(), await Model.findOne().lean(), etc.) from your schema definition, you can use Mongoose's inferRawDocType helper as follows:

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) {
  // ...
}

如果自动类型推断对你不起作用,你可以随时退回到文档接口定义。

¥If automatic type inference doesn't work for you, you can always fall back to document interface definitions.

单独的文档接口定义

¥Separate document interface definition

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

¥If automatic type inference doesn't work for you, you can define a separate raw document interface as follows.

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 在文档界面中是可选的,但 requiredschema 中是可选的,则上面的代码不会抛出错误。

¥By default, Mongoose does not check if your raw document interface lines up with your schema. For example, the above code won't throw an error if email is optional in the document interface, but required in schema.

通用参数

¥Generic parameters

TypeScript 中的 Mongoose Schema 类有 9 个 通用参数

¥The Mongoose Schema class in TypeScript has 9 generic parameters:

  • RawDocType - 描述数据如何在 MongoDB 中保存的接口

    ¥RawDocType - An interface describing how the data is saved in MongoDB

  • TModelType - Mongoose 模型类型。如果没有要定义的查询助手或实例方法,则可以省略。

    ¥TModelType - The Mongoose model type. Can be omitted if there are no query helpers or instance methods to be defined.

    • 默认:Model<DocType, any, any>

      ¥default: Model<DocType, any, any>

  • TInstanceMethods - 包含结构方法的接口。

    ¥TInstanceMethods - An interface containing the methods for the schema.

    • 默认:{}

      ¥default: {}

  • TQueryHelpers - 包含在结构上定义的查询助手的接口。默认为 {}

    ¥TQueryHelpers - An interface containing query helpers defined on the schema. Defaults to {}.

  • TVirtuals - 包含模式上定义的虚拟的接口。默认为 {}

    ¥TVirtuals - An interface containing virtuals defined on the schema. Defaults to {}

  • TStaticMethods - 包含模型上的方法的接口。默认为 {}

    ¥TStaticMethods - An interface containing methods on a model. Defaults to {}

  • TSchemaOptions - 作为第二个选项传递给 Schema() 构造函数的类型。默认为 DefaultSchemaOptions

    ¥TSchemaOptions - The type passed as the 2nd option to Schema() constructor. Defaults to DefaultSchemaOptions.

  • DocType - 从模式推断出的文档类型。

    ¥DocType - The inferred document type from the schema.

  • THydratedDocumentType - 水合文档类型。这是 await Model.findOne()Model.hydrate() 等的默认返回类型。

    ¥THydratedDocumentType - The hydrated document type. This is the default return type for await Model.findOne(), Model.hydrate(), etc.

View TypeScript definition
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 参数等情况。例如:

¥The first generic param, DocType, represents the type of documents that Mongoose will store in MongoDB. Mongoose wraps DocType in a Mongoose document for cases like the this parameter to document middleware. For example:

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

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

¥The second generic param, M, is the model used with the schema. Mongoose uses the M type in model middleware defined in the schema.

第三个通用参数 TInstanceMethods 用于为结构中定义的实例方法添加类型。

¥The third generic param, TInstanceMethods is used to add types for instance methods defined in the schema.

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

¥The 4th param, TQueryHelpers, is used to add types for chainable query helpers.

结构与接口字段

¥Schema vs Interface fields

Mongoose 检查以确保结构中的每个路径都在文档界面中定义。

¥Mongoose checks to make sure that every path in your schema is defined in your document interface.

例如,以下代码将无法编译,因为 email 是结构中的路径,但不是 DocType 接口中的路径。

¥For example, the below code will fail to compile because email is a path in the schema, but not in the DocType interface.

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 不会检查文档界面中存在的路径,但不会检查架构中存在的路径。例如,下面的代码可以编译。

¥However, Mongoose does not check for paths that exist in the document interface, but not in the schema. For example, the below code compiles.

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() 构造函数中。例如,timestampsplugins

¥This is because Mongoose has numerous features that add paths to your schema that should be included in the DocType interface without you explicitly putting these paths in the Schema() constructor. For example, timestamps and plugins.

数组

¥Arrays

当你在文档界面中定义数组时,我们建议使用原始 JavaScript 数组,而不是 Mongoose 的 Types.Array 类型或 Types.DocumentArray 类型。相反,使用模型和架构的 THydratedDocumentType 泛型来定义水合文档类型具有 Types.ArrayTypes.DocumentArray 类型的路径。

¥When you define an array in a document interface, we recommend using vanilla JavaScript arrays, not Mongoose's Types.Array type or Types.DocumentArray type. Instead, use the THydratedDocumentType generic for models and schemas to define that the hydrated document type has paths of type Types.Array and Types.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> 作为单个子文档的类型。

¥Use HydratedArraySubdocument<RawDocType> for the type of array subdocuments, and HydratedSingleSubdocument<RawDocType> for single subdocuments.

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

¥If you are not using schema methods, middleware, or virtuals, you can omit the last 7 generic parameters to Schema() and just define your schema using new mongoose.Schema<IOrder, OrderModelType>(...). The THydratedDocumentType parameter for schemas is primarily for setting the value of this on methods and virtuals.