Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add inferRawDocType helper #13900

Merged
merged 8 commits into from
May 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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;
}