Skip to content

Commit f63388f

Browse files
authored
Merge pull request #13900 from Automattic/vkarpov15/gh-13772
Add inferRawDocType helper
2 parents 5103366 + 3660041 commit f63388f

File tree

5 files changed

+161
-2
lines changed

5 files changed

+161
-2
lines changed

docs/typescript/schemas.md

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ Mongoose can automatically infer the document type from your schema definition a
99
We recommend relying on automatic type inference when defining schemas and models.
1010

1111
```typescript
12-
import { Schema } from 'mongoose';
12+
import { Schema, model } from 'mongoose';
1313
// Schema
1414
const schema = new Schema({
1515
name: { type: String, required: true },
@@ -32,6 +32,31 @@ There are a few caveats for using automatic type inference:
3232
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.
3333
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.
3434

35+
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:
36+
37+
```ts
38+
import { Schema, InferRawDocType, model } from 'mongoose';
39+
40+
const schemaDefinition = {
41+
name: { type: String, required: true },
42+
email: { type: String, required: true },
43+
avatar: String
44+
} as const;
45+
const schema = new Schema(schemaDefinition);
46+
47+
const UserModel = model('User', schema);
48+
const doc = new UserModel({ name: 'test', email: 'test' });
49+
50+
type RawUserDocument = InferRawDocType<typeof schemaDefinition>;
51+
52+
useRawDoc(doc.toObject());
53+
54+
function useRawDoc(doc: RawUserDocument) {
55+
// ...
56+
}
57+
58+
```
59+
3560
If automatic type inference doesn't work for you, you can always fall back to document interface definitions.
3661

3762
## Separate document interface definition

scripts/tsc-diagnostics-check.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
const fs = require('fs');
44

55
const stdin = fs.readFileSync(0).toString('utf8');
6-
const maxInstantiations = isNaN(process.argv[2]) ? 125000 : parseInt(process.argv[2], 10);
6+
const maxInstantiations = isNaN(process.argv[2]) ? 127500 : parseInt(process.argv[2], 10);
77

88
console.log(stdin);
99

test/types/schema.test.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
HydratedDocument,
77
IndexDefinition,
88
IndexOptions,
9+
InferRawDocType,
910
InferSchemaType,
1011
InsertManyOptions,
1112
ObtainDocumentType,
@@ -1494,3 +1495,19 @@ function gh14573() {
14941495
const doc = new UserModel({ names: { _id: '0'.repeat(24), firstName: 'foo' } });
14951496
doc.names?.ownerDocument();
14961497
}
1498+
1499+
function gh13772() {
1500+
const schemaDefinition = {
1501+
name: String,
1502+
docArr: [{ name: String }]
1503+
};
1504+
const schema = new Schema(schemaDefinition);
1505+
type RawDocType = InferRawDocType<typeof schemaDefinition>;
1506+
expectAssignable<
1507+
{ name?: string | null, docArr?: Array<{ name?: string | null }> }
1508+
>({} as RawDocType);
1509+
1510+
const TestModel = model('User', schema);
1511+
const doc = new TestModel();
1512+
expectAssignable<RawDocType>(doc.toObject());
1513+
}

types/index.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
/// <reference path="./types.d.ts" />
2121
/// <reference path="./utility.d.ts" />
2222
/// <reference path="./validation.d.ts" />
23+
/// <reference path="./inferrawdoctype.d.ts" />
2324
/// <reference path="./inferschematype.d.ts" />
2425
/// <reference path="./virtuals.d.ts" />
2526
/// <reference path="./augmentations.d.ts" />

types/inferrawdoctype.d.ts

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
import {
2+
IsSchemaTypeFromBuiltinClass,
3+
RequiredPaths,
4+
OptionalPaths,
5+
PathWithTypePropertyBaseType,
6+
PathEnumOrString
7+
} from './inferschematype';
8+
9+
declare module 'mongoose' {
10+
export type InferRawDocType<
11+
DocDefinition,
12+
TSchemaOptions extends Record<any, any> = DefaultSchemaOptions
13+
> = {
14+
[
15+
K in keyof (RequiredPaths<DocDefinition, TSchemaOptions['typeKey']> &
16+
OptionalPaths<DocDefinition, TSchemaOptions['typeKey']>)
17+
]: ObtainRawDocumentPathType<DocDefinition[K], TSchemaOptions['typeKey']>;
18+
};
19+
20+
/**
21+
* @summary Obtains schema Path type.
22+
* @description Obtains Path type by separating path type from other options and calling {@link ResolvePathType}
23+
* @param {PathValueType} PathValueType Document definition path type.
24+
* @param {TypeKey} TypeKey A generic refers to document definition.
25+
*/
26+
type ObtainRawDocumentPathType<
27+
PathValueType,
28+
TypeKey extends string = DefaultTypeKey
29+
> = ResolveRawPathType<
30+
PathValueType extends PathWithTypePropertyBaseType<TypeKey> ? PathValueType[TypeKey] : PathValueType,
31+
PathValueType extends PathWithTypePropertyBaseType<TypeKey> ? Omit<PathValueType, TypeKey> : {},
32+
TypeKey
33+
>;
34+
35+
/**
36+
* Same as inferSchemaType, except:
37+
*
38+
* 1. Replace `Types.DocumentArray` and `Types.Array` with vanilla `Array`
39+
* 2. Replace `ObtainDocumentPathType` with `ObtainRawDocumentPathType`
40+
* 3. Replace `ResolvePathType` with `ResolveRawPathType`
41+
*
42+
* @summary Resolve path type by returning the corresponding type.
43+
* @param {PathValueType} PathValueType Document definition path type.
44+
* @param {Options} Options Document definition path options except path type.
45+
* @param {TypeKey} TypeKey A generic of literal string type."Refers to the property used for path type definition".
46+
* @returns Number, "Number" or "number" will be resolved to number type.
47+
*/
48+
type ResolveRawPathType<PathValueType, Options extends SchemaTypeOptions<PathValueType> = {}, TypeKey extends string = DefaultSchemaOptions['typeKey']> =
49+
PathValueType extends Schema ?
50+
InferSchemaType<PathValueType> :
51+
PathValueType extends (infer Item)[] ?
52+
IfEquals<Item, never, any[], Item extends Schema ?
53+
// If Item is a schema, infer its type.
54+
Array<InferSchemaType<Item>> :
55+
Item extends Record<TypeKey, any> ?
56+
Item[TypeKey] extends Function | String ?
57+
// If Item has a type key that's a string or a callable, it must be a scalar,
58+
// so we can directly obtain its path type.
59+
ObtainRawDocumentPathType<Item, TypeKey>[] :
60+
// If the type key isn't callable, then this is an array of objects, in which case
61+
// we need to call ObtainDocumentType to correctly infer its type.
62+
Array<ObtainDocumentType<Item, any, { typeKey: TypeKey }>> :
63+
IsSchemaTypeFromBuiltinClass<Item> extends true ?
64+
ObtainRawDocumentPathType<Item, TypeKey>[] :
65+
IsItRecordAndNotAny<Item> extends true ?
66+
Item extends Record<string, never> ?
67+
ObtainRawDocumentPathType<Item, TypeKey>[] :
68+
Array<ObtainDocumentType<Item, any, { typeKey: TypeKey }>> :
69+
ObtainRawDocumentPathType<Item, TypeKey>[]
70+
>:
71+
PathValueType extends ReadonlyArray<infer Item> ?
72+
IfEquals<Item, never, any[], Item extends Schema ?
73+
Array<InferSchemaType<Item>> :
74+
Item extends Record<TypeKey, any> ?
75+
Item[TypeKey] extends Function | String ?
76+
ObtainRawDocumentPathType<Item, TypeKey>[] :
77+
ObtainDocumentType<Item, any, { typeKey: TypeKey }>[]:
78+
IsSchemaTypeFromBuiltinClass<Item> extends true ?
79+
ObtainRawDocumentPathType<Item, TypeKey>[] :
80+
IsItRecordAndNotAny<Item> extends true ?
81+
Item extends Record<string, never> ?
82+
ObtainRawDocumentPathType<Item, TypeKey>[] :
83+
Array<ObtainDocumentType<Item, any, { typeKey: TypeKey }>> :
84+
ObtainRawDocumentPathType<Item, TypeKey>[]
85+
>:
86+
PathValueType extends StringSchemaDefinition ? PathEnumOrString<Options['enum']> :
87+
IfEquals<PathValueType, Schema.Types.String> extends true ? PathEnumOrString<Options['enum']> :
88+
IfEquals<PathValueType, String> extends true ? PathEnumOrString<Options['enum']> :
89+
PathValueType extends NumberSchemaDefinition ? Options['enum'] extends ReadonlyArray<any> ? Options['enum'][number] : number :
90+
IfEquals<PathValueType, Schema.Types.Number> extends true ? number :
91+
PathValueType extends DateSchemaDefinition ? Date :
92+
IfEquals<PathValueType, Schema.Types.Date> extends true ? Date :
93+
PathValueType extends typeof Buffer | 'buffer' | 'Buffer' | typeof Schema.Types.Buffer ? Buffer :
94+
PathValueType extends BooleanSchemaDefinition ? boolean :
95+
IfEquals<PathValueType, Schema.Types.Boolean> extends true ? boolean :
96+
PathValueType extends ObjectIdSchemaDefinition ? Types.ObjectId :
97+
IfEquals<PathValueType, Types.ObjectId> extends true ? Types.ObjectId :
98+
IfEquals<PathValueType, Schema.Types.ObjectId> extends true ? Types.ObjectId :
99+
PathValueType extends 'decimal128' | 'Decimal128' | typeof Schema.Types.Decimal128 ? Types.Decimal128 :
100+
IfEquals<PathValueType, Schema.Types.Decimal128> extends true ? Types.Decimal128 :
101+
IfEquals<PathValueType, Types.Decimal128> extends true ? Types.Decimal128 :
102+
IfEquals<PathValueType, Schema.Types.BigInt> extends true ? bigint :
103+
IfEquals<PathValueType, BigInt> extends true ? bigint :
104+
PathValueType extends 'bigint' | 'BigInt' | typeof Schema.Types.BigInt | typeof BigInt ? bigint :
105+
PathValueType extends 'uuid' | 'UUID' | typeof Schema.Types.UUID ? Buffer :
106+
IfEquals<PathValueType, Schema.Types.UUID> extends true ? Buffer :
107+
PathValueType extends MapConstructor | 'Map' ? Map<string, ResolveRawPathType<Options['of']>> :
108+
IfEquals<PathValueType, typeof Schema.Types.Map> extends true ? Map<string, ResolveRawPathType<Options['of']>> :
109+
PathValueType extends ArrayConstructor ? any[] :
110+
PathValueType extends typeof Schema.Types.Mixed ? any:
111+
IfEquals<PathValueType, ObjectConstructor> extends true ? any:
112+
IfEquals<PathValueType, {}> extends true ? any:
113+
PathValueType extends typeof SchemaType ? PathValueType['prototype'] :
114+
PathValueType extends Record<string, any> ? ObtainDocumentType<PathValueType, any, { typeKey: TypeKey }> :
115+
unknown;
116+
}

0 commit comments

Comments
 (0)