diff --git a/test/types/models.test.ts b/test/types/models.test.ts index 1633c8d35b5..218c4c90569 100644 --- a/test/types/models.test.ts +++ b/test/types/models.test.ts @@ -13,7 +13,9 @@ import mongoose, { Query, UpdateWriteOpResult, AggregateOptions, - StringSchemaDefinition + WithLevel1NestedPaths, + NestedPaths, + InferSchemaType } from 'mongoose'; import { expectAssignable, expectError, expectType } from 'tsd'; import { AutoTypedSchemaType, autoTypedSchema } from './schema.test'; @@ -914,3 +916,64 @@ async function gh14440() { } ]); } + +async function gh12064() { + const FooSchema = new Schema({ + one: { type: String } + }); + + const MyRecordSchema = new Schema({ + _id: { type: String }, + foo: { type: FooSchema }, + arr: [Number] + }); + + const MyRecord = model('MyRecord', MyRecordSchema); + + expectType<(string | null)[]>( + await MyRecord.distinct('foo.one').exec() + ); + expectType<(string | null)[]>( + await MyRecord.find().distinct('foo.one').exec() + ); + expectType(await MyRecord.distinct('foo.two').exec()); + expectType(await MyRecord.distinct('arr.0').exec()); +} + +function testWithLevel1NestedPaths() { + type Test1 = WithLevel1NestedPaths<{ + topLevel: number, + nested1Level: { + l2: string + }, + nested2Level: { + l2: { l3: boolean } + } + }>; + + expectType<{ + topLevel: number, + nested1Level: { l2: string }, + 'nested1Level.l2': string, + nested2Level: { l2: { l3: boolean } }, + 'nested2Level.l2': { l3: boolean } + }>({} as Test1); + + const FooSchema = new Schema({ + one: { type: String } + }); + + const schema = new Schema({ + _id: { type: String }, + foo: { type: FooSchema } + }); + + type InferredDocType = InferSchemaType; + + type Test2 = WithLevel1NestedPaths; + expectAssignable<{ + _id: string | null | undefined, + foo?: { one?: string | null | undefined } | null | undefined, + 'foo.one': string | null | undefined + }>({} as Test2); +} diff --git a/types/models.d.ts b/types/models.d.ts index 34f938c07eb..27c43612ac2 100644 --- a/types/models.d.ts +++ b/types/models.d.ts @@ -623,7 +623,11 @@ declare module 'mongoose' { field: DocKey, filter?: FilterQuery ): QueryWithHelpers< - Array : ResultType>, + Array< + DocKey extends keyof WithLevel1NestedPaths + ? WithoutUndefined[DocKey]>> + : ResultType + >, THydratedDocumentType, TQueryHelpers, TRawDocType, diff --git a/types/query.d.ts b/types/query.d.ts index e827ac2d76e..6b6fb6b8cd1 100644 --- a/types/query.d.ts +++ b/types/query.d.ts @@ -355,7 +355,18 @@ declare module 'mongoose' { distinct( field: DocKey, filter?: FilterQuery - ): QueryWithHelpers : ResultType>, DocType, THelpers, RawDocType, 'distinct', TInstanceMethods>; + ): QueryWithHelpers< + Array< + DocKey extends keyof WithLevel1NestedPaths + ? WithoutUndefined[DocKey]>> + : ResultType + >, + DocType, + THelpers, + RawDocType, + 'distinct', + TInstanceMethods + >; /** Specifies a `$elemMatch` query condition. When called with one argument, the most recent path passed to `where()` is used. */ elemMatch(path: K, val: any): this; diff --git a/types/utility.d.ts b/types/utility.d.ts index 016f2c48b07..7c6df561818 100644 --- a/types/utility.d.ts +++ b/types/utility.d.ts @@ -2,6 +2,26 @@ declare module 'mongoose' { type IfAny = 0 extends (1 & IFTYPE) ? THENTYPE : ELSETYPE; type IfUnknown = unknown extends IFTYPE ? THENTYPE : IFTYPE; + type WithLevel1NestedPaths = { + [P in K | NestedPaths, K>]: P extends K + ? T[P] + : P extends `${infer Key}.${infer Rest}` + ? Key extends keyof T + ? Rest extends keyof NonNullable + ? NonNullable[Rest] + : never + : never + : never; + }; + + type NestedPaths = K extends string + ? T[K] extends Record | null | undefined + ? `${K}.${keyof NonNullable & string}` + : never + : never; + + type WithoutUndefined = T extends undefined ? never : T; + /** * @summary Removes keys from a type * @description It helps to exclude keys from a type