Skip to content

Commit

Permalink
Merge pull request #13900 from Automattic/vkarpov15/gh-13772
Browse files Browse the repository at this point in the history
Add inferRawDocType helper
  • Loading branch information
vkarpov15 committed May 15, 2024
2 parents 5103366 + 3660041 commit f63388f
Show file tree
Hide file tree
Showing 5 changed files with 161 additions and 2 deletions.
27 changes: 26 additions & 1 deletion docs/typescript/schemas.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 },
Expand All @@ -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<typeof schemaDefinition>;

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
Expand Down
2 changes: 1 addition & 1 deletion scripts/tsc-diagnostics-check.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down
17 changes: 17 additions & 0 deletions test/types/schema.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
HydratedDocument,
IndexDefinition,
IndexOptions,
InferRawDocType,
InferSchemaType,
InsertManyOptions,
ObtainDocumentType,
Expand Down Expand Up @@ -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<typeof schemaDefinition>;
expectAssignable<
{ name?: string | null, docArr?: Array<{ name?: string | null }> }
>({} as RawDocType);

const TestModel = model('User', schema);
const doc = new TestModel();
expectAssignable<RawDocType>(doc.toObject());
}
1 change: 1 addition & 0 deletions types/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
/// <reference path="./types.d.ts" />
/// <reference path="./utility.d.ts" />
/// <reference path="./validation.d.ts" />
/// <reference path="./inferrawdoctype.d.ts" />
/// <reference path="./inferschematype.d.ts" />
/// <reference path="./virtuals.d.ts" />
/// <reference path="./augmentations.d.ts" />
Expand Down
116 changes: 116 additions & 0 deletions types/inferrawdoctype.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import {
IsSchemaTypeFromBuiltinClass,
RequiredPaths,
OptionalPaths,
PathWithTypePropertyBaseType,
PathEnumOrString
} from './inferschematype';

declare module 'mongoose' {
export type InferRawDocType<
DocDefinition,
TSchemaOptions extends Record<any, any> = DefaultSchemaOptions
> = {
[
K in keyof (RequiredPaths<DocDefinition, TSchemaOptions['typeKey']> &
OptionalPaths<DocDefinition, TSchemaOptions['typeKey']>)
]: ObtainRawDocumentPathType<DocDefinition[K], TSchemaOptions['typeKey']>;
};

/**
* @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<TypeKey> ? PathValueType[TypeKey] : PathValueType,
PathValueType extends PathWithTypePropertyBaseType<TypeKey> ? Omit<PathValueType, TypeKey> : {},
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<PathValueType, Options extends SchemaTypeOptions<PathValueType> = {}, TypeKey extends string = DefaultSchemaOptions['typeKey']> =
PathValueType extends Schema ?
InferSchemaType<PathValueType> :
PathValueType extends (infer Item)[] ?
IfEquals<Item, never, any[], Item extends Schema ?
// If Item is a schema, infer its type.
Array<InferSchemaType<Item>> :
Item extends Record<TypeKey, any> ?
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<Item, TypeKey>[] :
// 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<ObtainDocumentType<Item, any, { typeKey: TypeKey }>> :
IsSchemaTypeFromBuiltinClass<Item> extends true ?
ObtainRawDocumentPathType<Item, TypeKey>[] :
IsItRecordAndNotAny<Item> extends true ?
Item extends Record<string, never> ?
ObtainRawDocumentPathType<Item, TypeKey>[] :
Array<ObtainDocumentType<Item, any, { typeKey: TypeKey }>> :
ObtainRawDocumentPathType<Item, TypeKey>[]
>:
PathValueType extends ReadonlyArray<infer Item> ?
IfEquals<Item, never, any[], Item extends Schema ?
Array<InferSchemaType<Item>> :
Item extends Record<TypeKey, any> ?
Item[TypeKey] extends Function | String ?
ObtainRawDocumentPathType<Item, TypeKey>[] :
ObtainDocumentType<Item, any, { typeKey: TypeKey }>[]:
IsSchemaTypeFromBuiltinClass<Item> extends true ?
ObtainRawDocumentPathType<Item, TypeKey>[] :
IsItRecordAndNotAny<Item> extends true ?
Item extends Record<string, never> ?
ObtainRawDocumentPathType<Item, TypeKey>[] :
Array<ObtainDocumentType<Item, any, { typeKey: TypeKey }>> :
ObtainRawDocumentPathType<Item, TypeKey>[]
>:
PathValueType extends StringSchemaDefinition ? PathEnumOrString<Options['enum']> :
IfEquals<PathValueType, Schema.Types.String> extends true ? PathEnumOrString<Options['enum']> :
IfEquals<PathValueType, String> extends true ? PathEnumOrString<Options['enum']> :
PathValueType extends NumberSchemaDefinition ? Options['enum'] extends ReadonlyArray<any> ? Options['enum'][number] : number :
IfEquals<PathValueType, Schema.Types.Number> extends true ? number :
PathValueType extends DateSchemaDefinition ? Date :
IfEquals<PathValueType, Schema.Types.Date> extends true ? Date :
PathValueType extends typeof Buffer | 'buffer' | 'Buffer' | typeof Schema.Types.Buffer ? Buffer :
PathValueType extends BooleanSchemaDefinition ? boolean :
IfEquals<PathValueType, Schema.Types.Boolean> extends true ? boolean :
PathValueType extends ObjectIdSchemaDefinition ? Types.ObjectId :
IfEquals<PathValueType, Types.ObjectId> extends true ? Types.ObjectId :
IfEquals<PathValueType, Schema.Types.ObjectId> extends true ? Types.ObjectId :
PathValueType extends 'decimal128' | 'Decimal128' | typeof Schema.Types.Decimal128 ? Types.Decimal128 :
IfEquals<PathValueType, Schema.Types.Decimal128> extends true ? Types.Decimal128 :
IfEquals<PathValueType, Types.Decimal128> extends true ? Types.Decimal128 :
IfEquals<PathValueType, Schema.Types.BigInt> extends true ? bigint :
IfEquals<PathValueType, BigInt> extends true ? bigint :
PathValueType extends 'bigint' | 'BigInt' | typeof Schema.Types.BigInt | typeof BigInt ? bigint :
PathValueType extends 'uuid' | 'UUID' | typeof Schema.Types.UUID ? Buffer :
IfEquals<PathValueType, Schema.Types.UUID> extends true ? Buffer :
PathValueType extends MapConstructor | 'Map' ? Map<string, ResolveRawPathType<Options['of']>> :
IfEquals<PathValueType, typeof Schema.Types.Map> extends true ? Map<string, ResolveRawPathType<Options['of']>> :
PathValueType extends ArrayConstructor ? any[] :
PathValueType extends typeof Schema.Types.Mixed ? any:
IfEquals<PathValueType, ObjectConstructor> extends true ? any:
IfEquals<PathValueType, {}> extends true ? any:
PathValueType extends typeof SchemaType ? PathValueType['prototype'] :
PathValueType extends Record<string, any> ? ObtainDocumentType<PathValueType, any, { typeKey: TypeKey }> :
unknown;
}

0 comments on commit f63388f

Please sign in to comment.