From 5c4051ac56b4414afe7a6a02f6b946c7360c5ea8 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Sat, 27 Apr 2024 13:48:30 -0400 Subject: [PATCH] docs: de-emphasize `InferSchemaType<>` in TypeScript docs in favor of automatic inference Re: #14286 --- docs/typescript/schemas.md | 139 +++++++++++++++++--------------- docs/typescript/subdocuments.md | 28 +++---- docs/typescript/virtuals.md | 2 +- types/index.d.ts | 16 ++-- 4 files changed, 98 insertions(+), 87 deletions(-) diff --git a/docs/typescript/schemas.md b/docs/typescript/schemas.md index 3966b278f69..660026e5c7c 100644 --- a/docs/typescript/schemas.md +++ b/docs/typescript/schemas.md @@ -1,89 +1,86 @@ # Schemas in TypeScript Mongoose [schemas](../guide.html) 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 *document interface* and a *schema*; or rely on Mongoose to automatically infer the type from the schema definition. +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. -## Separate document interface definition +## Automatic type inference + +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. ```typescript import { Schema } from 'mongoose'; - -// Document interface -interface User { - name: string; - email: string; - avatar?: string; -} - // Schema -const schema = new 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 ``` -By default, Mongoose does **not** check if your 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`. +There are a few caveats for using automatic type inference: -## Automatic type inference +1. 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](https://github.com/Automattic/mongoose/issues/12420) with automatic type inference with strict mode disabled. +2. 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. 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](https://github.com/Automattic/mongoose/issues/12807) 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. + +If automatic type inference doesn't work for you, you can always fall back to document interface definitions. -Mongoose can also automatically infer the document type from your schema definition as follows. +## Separate document interface definition + +If automatic type inference doesn't work for you, you can define a separate raw document interface as follows. ```typescript -import { Schema, InferSchemaType } from 'mongoose'; +import { Schema } from 'mongoose'; -// Document interface -// No need to define TS interface any more. -// interface User { -// name: string; -// email: string; -// avatar?: string; -// } +// 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({ +const schema = new Schema({ name: { type: String, required: true }, email: { type: String, required: true }, avatar: String }); - -type User = InferSchemaType; -// InferSchemaType will determine the type as follows: -// type User = { -// name: string; -// email: string; -// avatar?: string; -// } - -// `UserModel` will have `name: string`, etc. -const UserModel = mongoose.model('User', schema); ``` -There are a few caveats for using automatic type inference: - -1. 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](https://github.com/Automattic/mongoose/issues/12420) with automatic type inference with strict mode disabled. -2. 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. 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](https://github.com/Automattic/mongoose/issues/12807) 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. - -If automatic type inference doesn't work for you, you can always fall back to document interface definitions. +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 The Mongoose `Schema` class in TypeScript has 4 [generic parameters](https://www.typescriptlang.org/docs/handbook/2/generics.html): -* `DocType` - An interface describing how the data is saved in MongoDB +* `RawDocType` - An interface describing how the data is saved in MongoDB * `M` - The Mongoose model type. Can be omitted if there are no query helpers or instance methods to be defined. * default: `Model` * `TInstanceMethods` - An interface containing the methods for the schema. * default: `{}` * `TQueryHelpers` - An interface containing query helpers defined on the schema. Defaults to `{}`. +* `TVirtuals` - An interface containing virtuals defined on the schema. Defaults to `{}` +* `TSchemaOptions` - The type passed as the 2nd option to `Schema()` constructor. Defaults to `DefaultSchemaOptions`. +* `DocType` - The inferred document type from the schema. +* `THydratedDocumentType` - The hydrated document type. This is the default return type for `await Model.findOne()`, `Model.hydrate()`, etc.
View TypeScript definition ```typescript - class Schema, TInstanceMethods = {}, TQueryHelpers = {}> extends events.EventEmitter { + class Schema, TInstanceMethods = {}, TQueryHelpers = {}> extends events.EventEmitter { // ... } ``` @@ -154,33 +151,47 @@ This is because Mongoose has numerous features that add paths to your schema tha ## Arrays -When you define an array in a document interface, we recommend using Mongoose's `Types.Array` type for primitive arrays or `Types.DocumentArray` for arrays of documents. +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 to define that the hydrated document type has paths of type `Types.Array` and `Types.DocumentArray`. ```typescript -import { Schema, Model, Types } from 'mongoose'; +import mongoose from 'mongoose' +const { Schema } = mongoose; -interface BlogPost { - _id: Types.ObjectId; - title: string; +interface IOrder { + tags: Array<{ name: string }> } -interface User { - tags: Types.Array; - blogPosts: Types.DocumentArray; -} - -const schema = new Schema>({ - tags: [String], - blogPosts: [{ title: 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.Types.DocumentArray<{ name: string }> } +>; +type OrderModelType = mongoose.Model< + IOrder, + {}, + {}, + {}, + OrderHydratedDocument +>; + +const orderSchema = new mongoose.Schema({ + tags: [{ name: { type: String, required: true } }] }); -``` +const OrderModel = mongoose.model('Order', orderSchema); -Using `Types.DocumentArray` is helpful when dealing with defaults. -For example, `BlogPost` has an `_id` property that Mongoose will set by default. -If you use `Types.DocumentArray` in the above case, you'll be able to `push()` a subdocument without an `_id`. +// Demonstrating return types from OrderModel +const doc = new OrderModel({ tags: [{ name: 'test' }] }); -```typescript -const user = new User({ blogPosts: [] }); +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 }> -user.blogPosts.push({ title: 'test' }); // Would not work if you did `blogPosts: BlogPost[]` + const leanDoc = await OrderModel.findOne().orFail().lean(); + leanDoc.tags; // Array<{ name: string }> +}; ``` diff --git a/docs/typescript/subdocuments.md b/docs/typescript/subdocuments.md index 18fd7afc522..49edbb4ca27 100644 --- a/docs/typescript/subdocuments.md +++ b/docs/typescript/subdocuments.md @@ -34,23 +34,23 @@ doc.names.ownerDocument(); ``` Mongoose provides a mechanism to override types in the hydrated document. -The 3rd generic param to the `Model<>` is called `TMethodsAndOverrides`: originally it was just used to define methods, but you can also use it to override types as shown below. +Define a separate `THydratedDocumentType` and pass it as the 5th generic param to `mongoose.Model<>`. +`THydratedDocumentType` controls what type Mongoose uses for "hydrated documents", that is, what `await UserModel.findOne()`, `UserModel.hydrate()`, and `new UserModel()` return. ```ts // Define property overrides for hydrated documents -type UserDocumentOverrides = { - names: Types.Subdocument & Names; -}; -type UserModelType = Model; +type THydratedUserDocument = { + names?: mongoose.Types.Subdocument +} +type UserModelType = mongoose.Model; -const userSchema = new Schema({ - names: new Schema({ firstName: String }) +const userSchema = new mongoose.Schema({ + names: new mongoose.Schema({ firstName: String }) }); -const UserModel = model('User', userSchema); - +const UserModel = mongoose.model('User', userSchema); const doc = new UserModel({ names: { _id: '0'.repeat(24), firstName: 'foo' } }); -doc.names.ownerDocument(); // Works, `names` is a subdocument! +doc.names!.ownerDocument(); // Works, `names` is a subdocument! ``` ## Subdocument Arrays @@ -69,10 +69,10 @@ interface User { } // TMethodsAndOverrides -type UserDocumentProps = { - names: Types.DocumentArray; -}; -type UserModelType = Model; +type THydratedUserDocument = { + names?: Types.DocumentArray +} +type UserModelType = Model; // Create model const UserModel = model('User', new Schema({ diff --git a/docs/typescript/virtuals.md b/docs/typescript/virtuals.md index e99d05ad767..c59faba3e2d 100644 --- a/docs/typescript/virtuals.md +++ b/docs/typescript/virtuals.md @@ -29,7 +29,7 @@ const schema = new Schema( ``` Note that Mongoose does **not** include virtuals in the returned type from `InferSchemaType`. -That is because `InferSchemaType` returns the "raw" document interface, which represents the structure of the data stored in MongoDB. +That is because `InferSchemaType` returns a value similar to the raw document interface, which represents the structure of the data stored in MongoDB. ```ts type User = InferSchemaType; diff --git a/types/index.d.ts b/types/index.d.ts index 0402bfa32fa..cd5695b35f2 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -217,18 +217,18 @@ declare module 'mongoose' { TStaticMethods> = (schema: Schema, opts?: any) => void; export class Schema< - EnforcedDocType = any, - TModelType = Model, + RawDocType = any, + TModelType = Model, TInstanceMethods = {}, TQueryHelpers = {}, TVirtuals = {}, TStaticMethods = {}, TSchemaOptions = DefaultSchemaOptions, DocType extends ApplySchemaOptions< - ObtainDocumentType>, + ObtainDocumentType>, ResolveSchemaOptions > = ApplySchemaOptions< - ObtainDocumentType>, + ObtainDocumentType>, ResolveSchemaOptions >, THydratedDocumentType = HydratedDocument, TVirtuals & TInstanceMethods> @@ -237,10 +237,10 @@ declare module 'mongoose' { /** * Create a new schema */ - constructor(definition?: SchemaDefinition, EnforcedDocType> | DocType, options?: SchemaOptions, TInstanceMethods, TQueryHelpers, TStaticMethods, TVirtuals, THydratedDocumentType> | ResolveSchemaOptions); + constructor(definition?: SchemaDefinition, RawDocType> | DocType, options?: SchemaOptions, TInstanceMethods, TQueryHelpers, TStaticMethods, TVirtuals, THydratedDocumentType> | ResolveSchemaOptions); /** Adds key path / schema type pairs to this schema. */ - add(obj: SchemaDefinition> | Schema, prefix?: string): this; + add(obj: SchemaDefinition> | Schema, prefix?: string): this; /** * Add an alias for `path`. This means getting or setting the `alias` @@ -308,14 +308,14 @@ declare module 'mongoose' { methods: AddThisParameter & AnyObject; /** The original object passed to the schema constructor */ - obj: SchemaDefinition, EnforcedDocType>; + obj: SchemaDefinition, RawDocType>; /** Returns a new schema that has the `paths` from the original schema, minus the omitted ones. */ omit(paths: string[], options?: SchemaOptions): T; /** Gets/sets schema paths. */ path>(path: string): ResultType; - path(path: pathGeneric): SchemaType; + path(path: pathGeneric): SchemaType; path(path: string, constructor: any): this; /** Lists all paths and their type in the schema. */