Skip to content

Commit 72da808

Browse files
authored
Merge pull request #14542 from Automattic/vkarpov15/gh-14286
docs: de-emphasize `InferSchemaType<>` in TypeScript docs in favor of automatic inference
2 parents 434dac8 + d5bd91d commit 72da808

File tree

4 files changed

+112
-89
lines changed

4 files changed

+112
-89
lines changed

docs/typescript/schemas.md

+89-66
Original file line numberDiff line numberDiff line change
@@ -1,89 +1,98 @@
11
# Schemas in TypeScript
22

33
Mongoose [schemas](../guide.html) are how you tell Mongoose what your documents look like.
4-
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.
4+
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.
55

6-
## Separate document interface definition
6+
## Automatic type inference
7+
8+
Mongoose can automatically infer the document type from your schema definition as follows.
9+
We recommend relying on automatic type inference when defining schemas and models.
710

811
```typescript
912
import { Schema } from 'mongoose';
10-
11-
// Document interface
12-
interface User {
13-
name: string;
14-
email: string;
15-
avatar?: string;
16-
}
17-
1813
// Schema
19-
const schema = new Schema<User>({
14+
const schema = new Schema({
2015
name: { type: String, required: true },
2116
email: { type: String, required: true },
2217
avatar: String
2318
});
19+
20+
// `UserModel` will have `name: string`, etc.
21+
const UserModel = mongoose.model('User', schema);
22+
23+
const doc = new UserModel({ name: 'test', email: 'test' });
24+
doc.name; // string
25+
doc.email; // string
26+
doc.avatar; // string | undefined | null
2427
```
2528

26-
By default, Mongoose does **not** check if your document interface lines up with your schema.
27-
For example, the above code won't throw an error if `email` is optional in the document interface, but `required` in `schema`.
29+
There are a few caveats for using automatic type inference:
2830

29-
## Automatic type inference
31+
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.
32+
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.
33+
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.
34+
35+
If automatic type inference doesn't work for you, you can always fall back to document interface definitions.
3036

31-
Mongoose can also automatically infer the document type from your schema definition as follows.
37+
## Separate document interface definition
38+
39+
If automatic type inference doesn't work for you, you can define a separate raw document interface as follows.
3240

3341
```typescript
34-
import { Schema, InferSchemaType } from 'mongoose';
42+
import { Schema } from 'mongoose';
3543

36-
// Document interface
37-
// No need to define TS interface any more.
38-
// interface User {
39-
// name: string;
40-
// email: string;
41-
// avatar?: string;
42-
// }
44+
// Raw document interface. Contains the data type as it will be stored
45+
// in MongoDB. So you can ObjectId, Buffer, and other custom primitive data types.
46+
// But no Mongoose document arrays or subdocuments.
47+
interface User {
48+
name: string;
49+
email: string;
50+
avatar?: string;
51+
}
4352

4453
// Schema
45-
const schema = new Schema({
54+
const schema = new Schema<User>({
4655
name: { type: String, required: true },
4756
email: { type: String, required: true },
4857
avatar: String
4958
});
50-
51-
type User = InferSchemaType<typeof schema>;
52-
// InferSchemaType will determine the type as follows:
53-
// type User = {
54-
// name: string;
55-
// email: string;
56-
// avatar?: string;
57-
// }
58-
59-
// `UserModel` will have `name: string`, etc.
60-
const UserModel = mongoose.model('User', schema);
6159
```
6260

63-
There are a few caveats for using automatic type inference:
64-
65-
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.
66-
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.
67-
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.
68-
69-
If automatic type inference doesn't work for you, you can always fall back to document interface definitions.
61+
By default, Mongoose does **not** check if your raw document interface lines up with your schema.
62+
For example, the above code won't throw an error if `email` is optional in the document interface, but `required` in `schema`.
7063

7164
## Generic parameters
7265

73-
The Mongoose `Schema` class in TypeScript has 4 [generic parameters](https://www.typescriptlang.org/docs/handbook/2/generics.html):
66+
The Mongoose `Schema` class in TypeScript has 9 [generic parameters](https://www.typescriptlang.org/docs/handbook/2/generics.html):
7467

75-
* `DocType` - An interface describing how the data is saved in MongoDB
76-
* `M` - The Mongoose model type. Can be omitted if there are no query helpers or instance methods to be defined.
68+
* `RawDocType` - An interface describing how the data is saved in MongoDB
69+
* `TModelType` - The Mongoose model type. Can be omitted if there are no query helpers or instance methods to be defined.
7770
* default: `Model<DocType, any, any>`
7871
* `TInstanceMethods` - An interface containing the methods for the schema.
7972
* default: `{}`
8073
* `TQueryHelpers` - An interface containing query helpers defined on the schema. Defaults to `{}`.
74+
* `TVirtuals` - An interface containing virtuals defined on the schema. Defaults to `{}`
75+
* `TStaticMethods` - An interface containing methods on a model. Defaults to `{}`
76+
* `TSchemaOptions` - The type passed as the 2nd option to `Schema()` constructor. Defaults to `DefaultSchemaOptions`.
77+
* `DocType` - The inferred document type from the schema.
78+
* `THydratedDocumentType` - The hydrated document type. This is the default return type for `await Model.findOne()`, `Model.hydrate()`, etc.
8179

8280
<details>
8381
<summary>View TypeScript definition</summary>
8482

8583
```typescript
86-
class Schema<DocType = any, M = Model<DocType, any, any>, TInstanceMethods = {}, TQueryHelpers = {}> extends events.EventEmitter {
84+
export class Schema<
85+
RawDocType = any,
86+
TModelType = Model<RawDocType, any, any, any>,
87+
TInstanceMethods = {},
88+
TQueryHelpers = {},
89+
TVirtuals = {},
90+
TStaticMethods = {},
91+
TSchemaOptions = DefaultSchemaOptions,
92+
DocType = ...,
93+
THydratedDocumentType = HydratedDocument<FlatRecord<DocType>, TVirtuals & TInstanceMethods>
94+
>
95+
extends events.EventEmitter {
8796
// ...
8897
}
8998
```
@@ -154,33 +163,47 @@ This is because Mongoose has numerous features that add paths to your schema tha
154163

155164
## Arrays
156165

157-
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.
166+
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.
167+
Instead, use the `THydratedDocumentType` generic to define that the hydrated document type has paths of type `Types.Array` and `Types.DocumentArray`.
158168

159169
```typescript
160-
import { Schema, Model, Types } from 'mongoose';
170+
import mongoose from 'mongoose'
171+
const { Schema } = mongoose;
161172

162-
interface BlogPost {
163-
_id: Types.ObjectId;
164-
title: string;
173+
interface IOrder {
174+
tags: Array<{ name: string }>
165175
}
166176

167-
interface User {
168-
tags: Types.Array<string>;
169-
blogPosts: Types.DocumentArray<BlogPost>;
170-
}
171-
172-
const schema = new Schema<User, Model<User>>({
173-
tags: [String],
174-
blogPosts: [{ title: String }]
177+
// Define a HydratedDocumentType that describes what type Mongoose should use
178+
// for fully hydrated docs returned from `findOne()`, etc.
179+
type OrderHydratedDocument = mongoose.HydratedDocument<
180+
IOrder,
181+
{ tags: mongoose.Types.DocumentArray<{ name: string }> }
182+
>;
183+
type OrderModelType = mongoose.Model<
184+
IOrder,
185+
{},
186+
{},
187+
{},
188+
OrderHydratedDocument
189+
>;
190+
191+
const orderSchema = new mongoose.Schema<IOrder, OrderModelType>({
192+
tags: [{ name: { type: String, required: true } }]
175193
});
176-
```
194+
const OrderModel = mongoose.model<IOrder, OrderModelType>('Order', orderSchema);
177195

178-
Using `Types.DocumentArray` is helpful when dealing with defaults.
179-
For example, `BlogPost` has an `_id` property that Mongoose will set by default.
180-
If you use `Types.DocumentArray` in the above case, you'll be able to `push()` a subdocument without an `_id`.
196+
// Demonstrating return types from OrderModel
197+
const doc = new OrderModel({ tags: [{ name: 'test' }] });
181198

182-
```typescript
183-
const user = new User({ blogPosts: [] });
199+
doc.tags; // mongoose.Types.DocumentArray<{ name: string }>
200+
doc.toObject().tags; // Array<{ name: string }>
201+
202+
async function run() {
203+
const docFromDb = await OrderModel.findOne().orFail();
204+
docFromDb.tags; // mongoose.Types.DocumentArray<{ name: string }>
184205

185-
user.blogPosts.push({ title: 'test' }); // Would not work if you did `blogPosts: BlogPost[]`
206+
const leanDoc = await OrderModel.findOne().orFail().lean();
207+
leanDoc.tags; // Array<{ name: string }>
208+
};
186209
```

docs/typescript/subdocuments.md

+14-14
Original file line numberDiff line numberDiff line change
@@ -34,23 +34,23 @@ doc.names.ownerDocument();
3434
```
3535

3636
Mongoose provides a mechanism to override types in the hydrated document.
37-
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.
37+
Define a separate `THydratedDocumentType` and pass it as the 5th generic param to `mongoose.Model<>`.
38+
`THydratedDocumentType` controls what type Mongoose uses for "hydrated documents", that is, what `await UserModel.findOne()`, `UserModel.hydrate()`, and `new UserModel()` return.
3839

3940
```ts
4041
// Define property overrides for hydrated documents
41-
type UserDocumentOverrides = {
42-
names: Types.Subdocument<Types.ObjectId> & Names;
43-
};
44-
type UserModelType = Model<User, {}, UserDocumentOverrides>;
42+
type THydratedUserDocument = {
43+
names?: mongoose.Types.Subdocument<Names>
44+
}
45+
type UserModelType = mongoose.Model<User, {}, {}, {}, THydratedUserDocument>;
4546

46-
const userSchema = new Schema<User, UserModelType>({
47-
names: new Schema<Names>({ firstName: String })
47+
const userSchema = new mongoose.Schema<User, UserModelType>({
48+
names: new mongoose.Schema<Names>({ firstName: String })
4849
});
49-
const UserModel = model<User, UserModelType>('User', userSchema);
50-
50+
const UserModel = mongoose.model<User, UserModelType>('User', userSchema);
5151

5252
const doc = new UserModel({ names: { _id: '0'.repeat(24), firstName: 'foo' } });
53-
doc.names.ownerDocument(); // Works, `names` is a subdocument!
53+
doc.names!.ownerDocument(); // Works, `names` is a subdocument!
5454
```
5555

5656
## Subdocument Arrays
@@ -69,10 +69,10 @@ interface User {
6969
}
7070

7171
// TMethodsAndOverrides
72-
type UserDocumentProps = {
73-
names: Types.DocumentArray<Names>;
74-
};
75-
type UserModelType = Model<User, {}, UserDocumentProps>;
72+
type THydratedUserDocument = {
73+
names?: Types.DocumentArray<Names>
74+
}
75+
type UserModelType = Model<User, {}, {}, {}, THydratedUserDocument>;
7676

7777
// Create model
7878
const UserModel = model<User, UserModelType>('User', new Schema<User, UserModelType>({

docs/typescript/virtuals.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ const schema = new Schema(
2929
```
3030

3131
Note that Mongoose does **not** include virtuals in the returned type from `InferSchemaType`.
32-
That is because `InferSchemaType` returns the "raw" document interface, which represents the structure of the data stored in MongoDB.
32+
That is because `InferSchemaType` returns a value similar to the raw document interface, which represents the structure of the data stored in MongoDB.
3333

3434
```ts
3535
type User = InferSchemaType<typeof schema>;

types/index.d.ts

+8-8
Original file line numberDiff line numberDiff line change
@@ -217,18 +217,18 @@ declare module 'mongoose' {
217217
TStaticMethods> = (schema: Schema<DocType, M, TInstanceMethods, TQueryHelpers, TVirtuals, TStaticMethods>, opts?: any) => void;
218218

219219
export class Schema<
220-
EnforcedDocType = any,
221-
TModelType = Model<EnforcedDocType, any, any, any>,
220+
RawDocType = any,
221+
TModelType = Model<RawDocType, any, any, any>,
222222
TInstanceMethods = {},
223223
TQueryHelpers = {},
224224
TVirtuals = {},
225225
TStaticMethods = {},
226226
TSchemaOptions = DefaultSchemaOptions,
227227
DocType extends ApplySchemaOptions<
228-
ObtainDocumentType<DocType, EnforcedDocType, ResolveSchemaOptions<TSchemaOptions>>,
228+
ObtainDocumentType<DocType, RawDocType, ResolveSchemaOptions<TSchemaOptions>>,
229229
ResolveSchemaOptions<TSchemaOptions>
230230
> = ApplySchemaOptions<
231-
ObtainDocumentType<any, EnforcedDocType, ResolveSchemaOptions<TSchemaOptions>>,
231+
ObtainDocumentType<any, RawDocType, ResolveSchemaOptions<TSchemaOptions>>,
232232
ResolveSchemaOptions<TSchemaOptions>
233233
>,
234234
THydratedDocumentType = HydratedDocument<FlatRecord<DocType>, TVirtuals & TInstanceMethods>
@@ -237,10 +237,10 @@ declare module 'mongoose' {
237237
/**
238238
* Create a new schema
239239
*/
240-
constructor(definition?: SchemaDefinition<SchemaDefinitionType<EnforcedDocType>, EnforcedDocType> | DocType, options?: SchemaOptions<FlatRecord<DocType>, TInstanceMethods, TQueryHelpers, TStaticMethods, TVirtuals, THydratedDocumentType> | ResolveSchemaOptions<TSchemaOptions>);
240+
constructor(definition?: SchemaDefinition<SchemaDefinitionType<RawDocType>, RawDocType> | DocType, options?: SchemaOptions<FlatRecord<DocType>, TInstanceMethods, TQueryHelpers, TStaticMethods, TVirtuals, THydratedDocumentType> | ResolveSchemaOptions<TSchemaOptions>);
241241

242242
/** Adds key path / schema type pairs to this schema. */
243-
add(obj: SchemaDefinition<SchemaDefinitionType<EnforcedDocType>> | Schema, prefix?: string): this;
243+
add(obj: SchemaDefinition<SchemaDefinitionType<RawDocType>> | Schema, prefix?: string): this;
244244

245245
/**
246246
* Add an alias for `path`. This means getting or setting the `alias`
@@ -308,14 +308,14 @@ declare module 'mongoose' {
308308
methods: AddThisParameter<TInstanceMethods, THydratedDocumentType> & AnyObject;
309309

310310
/** The original object passed to the schema constructor */
311-
obj: SchemaDefinition<SchemaDefinitionType<EnforcedDocType>, EnforcedDocType>;
311+
obj: SchemaDefinition<SchemaDefinitionType<RawDocType>, RawDocType>;
312312

313313
/** Returns a new schema that has the `paths` from the original schema, minus the omitted ones. */
314314
omit<T = this>(paths: string[], options?: SchemaOptions): T;
315315

316316
/** Gets/sets schema paths. */
317317
path<ResultType extends SchemaType = SchemaType<any, THydratedDocumentType>>(path: string): ResultType;
318-
path<pathGeneric extends keyof EnforcedDocType>(path: pathGeneric): SchemaType<EnforcedDocType[pathGeneric]>;
318+
path<pathGeneric extends keyof RawDocType>(path: pathGeneric): SchemaType<RawDocType[pathGeneric]>;
319319
path(path: string, constructor: any): this;
320320

321321
/** Lists all paths and their type in the schema. */

0 commit comments

Comments
 (0)