|
1 | 1 | # Schemas in TypeScript
|
2 | 2 |
|
3 | 3 | 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. |
5 | 5 |
|
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. |
7 | 10 |
|
8 | 11 | ```typescript
|
9 | 12 | import { Schema } from 'mongoose';
|
10 |
| - |
11 |
| -// Document interface |
12 |
| -interface User { |
13 |
| - name: string; |
14 |
| - email: string; |
15 |
| - avatar?: string; |
16 |
| -} |
17 |
| - |
18 | 13 | // Schema
|
19 |
| -const schema = new Schema<User>({ |
| 14 | +const schema = new Schema({ |
20 | 15 | name: { type: String, required: true },
|
21 | 16 | email: { type: String, required: true },
|
22 | 17 | avatar: String
|
23 | 18 | });
|
| 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 |
24 | 27 | ```
|
25 | 28 |
|
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: |
28 | 30 |
|
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. |
30 | 36 |
|
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. |
32 | 40 |
|
33 | 41 | ```typescript
|
34 |
| -import { Schema, InferSchemaType } from 'mongoose'; |
| 42 | +import { Schema } from 'mongoose'; |
35 | 43 |
|
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 | +} |
43 | 52 |
|
44 | 53 | // Schema
|
45 |
| -const schema = new Schema({ |
| 54 | +const schema = new Schema<User>({ |
46 | 55 | name: { type: String, required: true },
|
47 | 56 | email: { type: String, required: true },
|
48 | 57 | avatar: String
|
49 | 58 | });
|
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); |
61 | 59 | ```
|
62 | 60 |
|
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`. |
70 | 63 |
|
71 | 64 | ## Generic parameters
|
72 | 65 |
|
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): |
74 | 67 |
|
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. |
77 | 70 | * default: `Model<DocType, any, any>`
|
78 | 71 | * `TInstanceMethods` - An interface containing the methods for the schema.
|
79 | 72 | * default: `{}`
|
80 | 73 | * `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. |
81 | 79 |
|
82 | 80 | <details>
|
83 | 81 | <summary>View TypeScript definition</summary>
|
84 | 82 |
|
85 | 83 | ```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 { |
87 | 96 | // ...
|
88 | 97 | }
|
89 | 98 | ```
|
@@ -154,33 +163,47 @@ This is because Mongoose has numerous features that add paths to your schema tha
|
154 | 163 |
|
155 | 164 | ## Arrays
|
156 | 165 |
|
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`. |
158 | 168 |
|
159 | 169 | ```typescript
|
160 |
| -import { Schema, Model, Types } from 'mongoose'; |
| 170 | +import mongoose from 'mongoose' |
| 171 | +const { Schema } = mongoose; |
161 | 172 |
|
162 |
| -interface BlogPost { |
163 |
| - _id: Types.ObjectId; |
164 |
| - title: string; |
| 173 | +interface IOrder { |
| 174 | + tags: Array<{ name: string }> |
165 | 175 | }
|
166 | 176 |
|
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 } }] |
175 | 193 | });
|
176 |
| -``` |
| 194 | +const OrderModel = mongoose.model<IOrder, OrderModelType>('Order', orderSchema); |
177 | 195 |
|
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' }] }); |
181 | 198 |
|
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 }> |
184 | 205 |
|
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 | +}; |
186 | 209 | ```
|
0 commit comments