diff --git a/docs/typescript/schemas.md b/docs/typescript/schemas.md index e4127ce1621..8dfa5310556 100644 --- a/docs/typescript/schemas.md +++ b/docs/typescript/schemas.md @@ -9,7 +9,7 @@ Mongoose can automatically infer the document type from your schema definition a We recommend relying on automatic type inference when defining schemas and models. ```typescript -import { Schema } from 'mongoose'; +import { Schema, model } from 'mongoose'; // Schema const schema = new Schema({ name: { type: String, required: true }, @@ -32,6 +32,31 @@ There are a few caveats for using automatic type inference: 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 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: + +```ts +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; + +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 diff --git a/scripts/tsc-diagnostics-check.js b/scripts/tsc-diagnostics-check.js index 2f74bf39b92..3e1f6c66282 100644 --- a/scripts/tsc-diagnostics-check.js +++ b/scripts/tsc-diagnostics-check.js @@ -3,7 +3,7 @@ const fs = require('fs'); const stdin = fs.readFileSync(0).toString('utf8'); -const maxInstantiations = isNaN(process.argv[2]) ? 125000 : parseInt(process.argv[2], 10); +const maxInstantiations = isNaN(process.argv[2]) ? 127500 : parseInt(process.argv[2], 10); console.log(stdin); diff --git a/test/types/schema.test.ts b/test/types/schema.test.ts index 8deb72a6eda..bbbcdd5d71f 100644 --- a/test/types/schema.test.ts +++ b/test/types/schema.test.ts @@ -6,6 +6,7 @@ import { HydratedDocument, IndexDefinition, IndexOptions, + InferRawDocType, InferSchemaType, InsertManyOptions, ObtainDocumentType, @@ -1494,3 +1495,19 @@ function gh14573() { const doc = new UserModel({ names: { _id: '0'.repeat(24), firstName: 'foo' } }); doc.names?.ownerDocument(); } + +function gh13772() { + const schemaDefinition = { + name: String, + docArr: [{ name: String }] + }; + const schema = new Schema(schemaDefinition); + type RawDocType = InferRawDocType; + expectAssignable< + { name?: string | null, docArr?: Array<{ name?: string | null }> } + >({} as RawDocType); + + const TestModel = model('User', schema); + const doc = new TestModel(); + expectAssignable(doc.toObject()); +} diff --git a/types/index.d.ts b/types/index.d.ts index 6f851d23dfa..2e2a5fbc6a7 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -20,6 +20,7 @@ /// /// /// +/// /// /// /// diff --git a/types/inferrawdoctype.d.ts b/types/inferrawdoctype.d.ts new file mode 100644 index 00000000000..95a912ff240 --- /dev/null +++ b/types/inferrawdoctype.d.ts @@ -0,0 +1,116 @@ +import { + IsSchemaTypeFromBuiltinClass, + RequiredPaths, + OptionalPaths, + PathWithTypePropertyBaseType, + PathEnumOrString +} from './inferschematype'; + +declare module 'mongoose' { + export type InferRawDocType< + DocDefinition, + TSchemaOptions extends Record = DefaultSchemaOptions + > = { + [ + K in keyof (RequiredPaths & + OptionalPaths) + ]: ObtainRawDocumentPathType; + }; + + /** + * @summary Obtains schema Path type. + * @description Obtains Path type by separating path type from other options and calling {@link ResolvePathType} + * @param {PathValueType} PathValueType Document definition path type. + * @param {TypeKey} TypeKey A generic refers to document definition. + */ + type ObtainRawDocumentPathType< + PathValueType, + TypeKey extends string = DefaultTypeKey + > = ResolveRawPathType< + PathValueType extends PathWithTypePropertyBaseType ? PathValueType[TypeKey] : PathValueType, + PathValueType extends PathWithTypePropertyBaseType ? Omit : {}, + TypeKey + >; + + /** + * Same as inferSchemaType, except: + * + * 1. Replace `Types.DocumentArray` and `Types.Array` with vanilla `Array` + * 2. Replace `ObtainDocumentPathType` with `ObtainRawDocumentPathType` + * 3. Replace `ResolvePathType` with `ResolveRawPathType` + * + * @summary Resolve path type by returning the corresponding type. + * @param {PathValueType} PathValueType Document definition path type. + * @param {Options} Options Document definition path options except path type. + * @param {TypeKey} TypeKey A generic of literal string type."Refers to the property used for path type definition". + * @returns Number, "Number" or "number" will be resolved to number type. + */ + type ResolveRawPathType = {}, TypeKey extends string = DefaultSchemaOptions['typeKey']> = + PathValueType extends Schema ? + InferSchemaType : + PathValueType extends (infer Item)[] ? + IfEquals> : + Item extends Record ? + Item[TypeKey] extends Function | String ? + // If Item has a type key that's a string or a callable, it must be a scalar, + // so we can directly obtain its path type. + ObtainRawDocumentPathType[] : + // If the type key isn't callable, then this is an array of objects, in which case + // we need to call ObtainDocumentType to correctly infer its type. + Array> : + IsSchemaTypeFromBuiltinClass extends true ? + ObtainRawDocumentPathType[] : + IsItRecordAndNotAny extends true ? + Item extends Record ? + ObtainRawDocumentPathType[] : + Array> : + ObtainRawDocumentPathType[] + >: + PathValueType extends ReadonlyArray ? + IfEquals> : + Item extends Record ? + Item[TypeKey] extends Function | String ? + ObtainRawDocumentPathType[] : + ObtainDocumentType[]: + IsSchemaTypeFromBuiltinClass extends true ? + ObtainRawDocumentPathType[] : + IsItRecordAndNotAny extends true ? + Item extends Record ? + ObtainRawDocumentPathType[] : + Array> : + ObtainRawDocumentPathType[] + >: + PathValueType extends StringSchemaDefinition ? PathEnumOrString : + IfEquals extends true ? PathEnumOrString : + IfEquals extends true ? PathEnumOrString : + PathValueType extends NumberSchemaDefinition ? Options['enum'] extends ReadonlyArray ? Options['enum'][number] : number : + IfEquals extends true ? number : + PathValueType extends DateSchemaDefinition ? Date : + IfEquals extends true ? Date : + PathValueType extends typeof Buffer | 'buffer' | 'Buffer' | typeof Schema.Types.Buffer ? Buffer : + PathValueType extends BooleanSchemaDefinition ? boolean : + IfEquals extends true ? boolean : + PathValueType extends ObjectIdSchemaDefinition ? Types.ObjectId : + IfEquals extends true ? Types.ObjectId : + IfEquals extends true ? Types.ObjectId : + PathValueType extends 'decimal128' | 'Decimal128' | typeof Schema.Types.Decimal128 ? Types.Decimal128 : + IfEquals extends true ? Types.Decimal128 : + IfEquals extends true ? Types.Decimal128 : + IfEquals extends true ? bigint : + IfEquals extends true ? bigint : + PathValueType extends 'bigint' | 'BigInt' | typeof Schema.Types.BigInt | typeof BigInt ? bigint : + PathValueType extends 'uuid' | 'UUID' | typeof Schema.Types.UUID ? Buffer : + IfEquals extends true ? Buffer : + PathValueType extends MapConstructor | 'Map' ? Map> : + IfEquals extends true ? Map> : + PathValueType extends ArrayConstructor ? any[] : + PathValueType extends typeof Schema.Types.Mixed ? any: + IfEquals extends true ? any: + IfEquals extends true ? any: + PathValueType extends typeof SchemaType ? PathValueType['prototype'] : + PathValueType extends Record ? ObtainDocumentType : + unknown; +}