Skip to content

Commit 6183560

Browse files
authored
Merge pull request #14632 from Automattic/vkarpov15/gh-14615
types(models+query): infer return type from schema for 1-level deep nested paths
2 parents c71ba5e + 2c45e85 commit 6183560

File tree

4 files changed

+101
-3
lines changed

4 files changed

+101
-3
lines changed

test/types/models.test.ts

Lines changed: 64 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,9 @@ import mongoose, {
1313
Query,
1414
UpdateWriteOpResult,
1515
AggregateOptions,
16-
StringSchemaDefinition
16+
WithLevel1NestedPaths,
17+
NestedPaths,
18+
InferSchemaType
1719
} from 'mongoose';
1820
import { expectAssignable, expectError, expectType } from 'tsd';
1921
import { AutoTypedSchemaType, autoTypedSchema } from './schema.test';
@@ -914,3 +916,64 @@ async function gh14440() {
914916
}
915917
]);
916918
}
919+
920+
async function gh12064() {
921+
const FooSchema = new Schema({
922+
one: { type: String }
923+
});
924+
925+
const MyRecordSchema = new Schema({
926+
_id: { type: String },
927+
foo: { type: FooSchema },
928+
arr: [Number]
929+
});
930+
931+
const MyRecord = model('MyRecord', MyRecordSchema);
932+
933+
expectType<(string | null)[]>(
934+
await MyRecord.distinct('foo.one').exec()
935+
);
936+
expectType<(string | null)[]>(
937+
await MyRecord.find().distinct('foo.one').exec()
938+
);
939+
expectType<unknown[]>(await MyRecord.distinct('foo.two').exec());
940+
expectType<unknown[]>(await MyRecord.distinct('arr.0').exec());
941+
}
942+
943+
function testWithLevel1NestedPaths() {
944+
type Test1 = WithLevel1NestedPaths<{
945+
topLevel: number,
946+
nested1Level: {
947+
l2: string
948+
},
949+
nested2Level: {
950+
l2: { l3: boolean }
951+
}
952+
}>;
953+
954+
expectType<{
955+
topLevel: number,
956+
nested1Level: { l2: string },
957+
'nested1Level.l2': string,
958+
nested2Level: { l2: { l3: boolean } },
959+
'nested2Level.l2': { l3: boolean }
960+
}>({} as Test1);
961+
962+
const FooSchema = new Schema({
963+
one: { type: String }
964+
});
965+
966+
const schema = new Schema({
967+
_id: { type: String },
968+
foo: { type: FooSchema }
969+
});
970+
971+
type InferredDocType = InferSchemaType<typeof schema>;
972+
973+
type Test2 = WithLevel1NestedPaths<InferredDocType>;
974+
expectAssignable<{
975+
_id: string | null | undefined,
976+
foo?: { one?: string | null | undefined } | null | undefined,
977+
'foo.one': string | null | undefined
978+
}>({} as Test2);
979+
}

types/models.d.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -623,7 +623,11 @@ declare module 'mongoose' {
623623
field: DocKey,
624624
filter?: FilterQuery<TRawDocType>
625625
): QueryWithHelpers<
626-
Array<DocKey extends keyof TRawDocType ? Unpacked<TRawDocType[DocKey]> : ResultType>,
626+
Array<
627+
DocKey extends keyof WithLevel1NestedPaths<TRawDocType>
628+
? WithoutUndefined<Unpacked<WithLevel1NestedPaths<TRawDocType>[DocKey]>>
629+
: ResultType
630+
>,
627631
THydratedDocumentType,
628632
TQueryHelpers,
629633
TRawDocType,

types/query.d.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -355,7 +355,18 @@ declare module 'mongoose' {
355355
distinct<DocKey extends string, ResultType = unknown>(
356356
field: DocKey,
357357
filter?: FilterQuery<RawDocType>
358-
): QueryWithHelpers<Array<DocKey extends keyof DocType ? Unpacked<DocType[DocKey]> : ResultType>, DocType, THelpers, RawDocType, 'distinct', TInstanceMethods>;
358+
): QueryWithHelpers<
359+
Array<
360+
DocKey extends keyof WithLevel1NestedPaths<DocType>
361+
? WithoutUndefined<Unpacked<WithLevel1NestedPaths<DocType>[DocKey]>>
362+
: ResultType
363+
>,
364+
DocType,
365+
THelpers,
366+
RawDocType,
367+
'distinct',
368+
TInstanceMethods
369+
>;
359370

360371
/** Specifies a `$elemMatch` query condition. When called with one argument, the most recent path passed to `where()` is used. */
361372
elemMatch<K = string>(path: K, val: any): this;

types/utility.d.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,26 @@ declare module 'mongoose' {
22
type IfAny<IFTYPE, THENTYPE, ELSETYPE = IFTYPE> = 0 extends (1 & IFTYPE) ? THENTYPE : ELSETYPE;
33
type IfUnknown<IFTYPE, THENTYPE> = unknown extends IFTYPE ? THENTYPE : IFTYPE;
44

5+
type WithLevel1NestedPaths<T, K extends keyof T = keyof T> = {
6+
[P in K | NestedPaths<Required<T>, K>]: P extends K
7+
? T[P]
8+
: P extends `${infer Key}.${infer Rest}`
9+
? Key extends keyof T
10+
? Rest extends keyof NonNullable<T[Key]>
11+
? NonNullable<T[Key]>[Rest]
12+
: never
13+
: never
14+
: never;
15+
};
16+
17+
type NestedPaths<T, K extends keyof T> = K extends string
18+
? T[K] extends Record<string, any> | null | undefined
19+
? `${K}.${keyof NonNullable<T[K]> & string}`
20+
: never
21+
: never;
22+
23+
type WithoutUndefined<T> = T extends undefined ? never : T;
24+
525
/**
626
* @summary Removes keys from a type
727
* @description It helps to exclude keys from a type

0 commit comments

Comments
 (0)