TypeScript 中的 Schema

Mongoose schema 定義了 Mongoose 如何看待您的文件。Mongoose schema 與 TypeScript 介面是分開的,因此您需要定義一個原始文件介面和一個schema;或者依靠 Mongoose 從 schema 定義自動推斷型別。

自動型別推斷

Mongoose 可以從您的 schema 定義中自動推斷文件型別,如下所示。我們建議在定義 schema 和模型時依賴自動型別推斷。

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() 呼叫中定義您的 schema。不要將您的 schema 定義指派給臨時變數。類似 const schemaDefinition = { name: String }; const schema = new Schema(schemaDefinition); 的做法將不起作用。
  3. 如果您在 schema 中指定 timestamps 選項,Mongoose 會將 createdAtupdatedAt 新增到您的 schema 中,除非您也指定 methodsvirtualsstatics。在時間戳記與方法/虛擬/靜態選項的型別推斷方面,存在已知問題。如果您使用方法、虛擬和靜態,您需要負責將 createdAtupdatedAt 新增到您的 schema 定義中。

如果您必須單獨定義您的 schema,請使用 as constconst schemaDefinition = { ... } as const;)來防止型別擴展。TypeScript 會自動將類似 required: false 的型別擴展為 required: boolean,這會導致 Mongoose 假設該欄位是必填的。使用 as const 會強制 TypeScript 保留這些型別。

如果您需要從您的 schema 定義中明確取得原始文件型別(從 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 不會檢查您的原始文件介面是否與您的 schema 一致。例如,如果 email 在文件介面中是選填的,但在 schema 中是 required,則上述程式碼不會擲回錯誤。

泛型參數

TypeScript 中的 Mongoose Schema 類別有 9 個 泛型參數

  • RawDocType - 一個描述資料如何在 MongoDB 中儲存的介面
  • TModelType - Mongoose 模型型別。如果沒有要定義的查詢輔助函數或實例方法,則可以省略。
    • 預設值:Model<DocType, any, any>
  • TInstanceMethods - 一個包含 schema 方法的介面。
    • 預設值:{}
  • TQueryHelpers - 一個包含在 schema 上定義的查詢輔助函數的介面。預設為 {}
  • TVirtuals - 一個包含在 schema 上定義的虛擬屬性的介面。預設為 {}
  • TStaticMethods - 一個包含模型方法的介面。預設為 {}
  • TSchemaOptions - 作為第二個選項傳遞給 Schema() 建構函式的型別。預設為 DefaultSchemaOptions
  • DocType - 從 schema 推斷的文件型別。
  • 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 中儲存的文件型別。對於類似於文件中介軟體的 this 參數的情況,Mongoose 會將 DocType 包裝在 Mongoose 文件中。例如

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

第二個泛型參數 M 是與 schema 一起使用的模型。Mongoose 在 schema 中定義的模型中介軟體中使用 M 型別。

第三個泛型參數 TInstanceMethods 用於新增 schema 中定義的實例方法型別。

第四個參數 TQueryHelpers 用於新增 可鏈接的查詢輔助函數型別。

Schema 與介面欄位

Mongoose 會檢查您 schema 中的每個路徑是否都在您的文件介面中定義。

例如,下面的程式碼將無法編譯,因為 email 是 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 Schema<User>({
  name: { type: String, required: true },
  emaill: { type: String, required: true },
  avatar: String
});

但是,Mongoose 不會檢查存在於文件介面中但不存在於 schema 中的路徑。例如,下面的程式碼會編譯。

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 有許多功能可以將路徑新增到您的 schema 中,這些路徑應該包含在 DocType 介面中,而無需您在 Schema() 建構函式中明確放置這些路徑。例如,時間戳記外掛

陣列

當您在文件介面中定義陣列時,我們建議使用原始 JavaScript 陣列,而不是 Mongoose 的 Types.Array 型別或 Types.DocumentArray 型別。而是使用模型和 schema 的 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 方法、中介軟體或虛擬屬性,您可以省略 Schema() 的最後 7 個泛型參數,只需使用 new mongoose.Schema<IOrder, OrderModelType>(...) 來定義您的 schema。Schema 的 THydratedDocumentType 參數主要用於設定方法和虛擬屬性中的 this 的值。