From 3eb6ccca11ce2016056bc23f5f02e4a508b795df Mon Sep 17 00:00:00 2001 From: Bailey Pearson Date: Tue, 11 Jan 2022 13:46:37 -0500 Subject: [PATCH 01/14] Extend Filter type to support querying on a document --- src/mongo_types.ts | 12 +++++---- .../community/collection/findX.test-d.ts | 27 ++++++++++++++++++- 2 files changed, 33 insertions(+), 6 deletions(-) diff --git a/src/mongo_types.ts b/src/mongo_types.ts index 60564a87cf..36fc7724f8 100644 --- a/src/mongo_types.ts +++ b/src/mongo_types.ts @@ -65,11 +65,13 @@ export type EnhancedOmit = string extends keyof TRecor export type WithoutId = Omit; /** A MongoDB filter can be some portion of the schema or a set of operators @public */ -export type Filter = { - [Property in Join>, '.'>]?: Condition< - PropertyType, Property> - >; -} & RootFilterOperators>; +export type Filter = + | Partial + | ({ + [Property in Join>, '.'>]?: Condition< + PropertyType, Property> + >; + } & RootFilterOperators>); /** @public */ export type Condition = AlternativeType | FilterOperators>; diff --git a/test/types/community/collection/findX.test-d.ts b/test/types/community/collection/findX.test-d.ts index 1310f1bf79..264695434f 100644 --- a/test/types/community/collection/findX.test-d.ts +++ b/test/types/community/collection/findX.test-d.ts @@ -1,6 +1,6 @@ import { expectAssignable, expectNotType, expectType } from 'tsd'; -import type { Projection, ProjectionOperators } from '../../../../src'; +import type { Filter, Projection, ProjectionOperators } from '../../../../src'; import { Collection, Db, @@ -300,3 +300,28 @@ expectAssignable(await schemaWithUserDefinedId.f // should allow _id as a number await schemaWithUserDefinedId.findOne({ _id: 5 }); await schemaWithUserDefinedId.find({ _id: 5 }); + +// We should be able to use a doc of type T as a filter object when performing findX operations +interface Foo { + a: string; +} + +const fooObj: Foo = { + a: 'john doe' +}; + +// eslint-disable-next-line @typescript-eslint/no-unused-vars +const fooFilter: Filter = fooObj; + +// Specifically test that arrays can be included as a part of an object +// ensuring that a bug reported in https://jira.mongodb.org/browse/NODE-3856 is addressed +interface FooWithArray { + a: number[]; +} + +const fooObjWithArray: FooWithArray = { + a: [1, 2, 3, 4] +}; + +// eslint-disable-next-line @typescript-eslint/no-unused-vars +const fooFilterWithArray: Filter = fooObjWithArray; From 89a4fc97b429b1c17ef5d3c303f4119c663f4adc Mon Sep 17 00:00:00 2001 From: Bailey Pearson Date: Tue, 11 Jan 2022 14:01:17 -0500 Subject: [PATCH 02/14] Fix the findOneAnd return types to include _id --- src/collection.ts | 2 +- .../community/collection/findX.test-d.ts | 23 +++++++++++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/src/collection.ts b/src/collection.ts index 73be2759e9..86429cbabb 100644 --- a/src/collection.ts +++ b/src/collection.ts @@ -102,7 +102,7 @@ import { WriteConcern, WriteConcernOptions } from './write_concern'; /** @public */ export interface ModifyResult { - value: TSchema | null; + value: WithId | null; lastErrorObject?: Document; ok: 0 | 1; } diff --git a/test/types/community/collection/findX.test-d.ts b/test/types/community/collection/findX.test-d.ts index 264695434f..463ba1518e 100644 --- a/test/types/community/collection/findX.test-d.ts +++ b/test/types/community/collection/findX.test-d.ts @@ -325,3 +325,26 @@ const fooObjWithArray: FooWithArray = { // eslint-disable-next-line @typescript-eslint/no-unused-vars const fooFilterWithArray: Filter = fooObjWithArray; + +declare const coll: Collection<{ a: number; b: string }>; +expectType | null>((await coll.findOneAndDelete({ a: 3 })).value); +expectType | null>( + (await coll.findOneAndReplace({ a: 3 }, { a: 5, b: 'new string' })).value +); +expectType | null>( + ( + await coll.findOneAndUpdate( + { a: 3 }, + { + $set: { + a: 5 + } + } + ) + ).value +); + +// projections do not change the return type - our typing doesn't support this +expectType | null>( + (await coll.findOneAndDelete({ a: 3 }, { projection: { _id: 0 } })).value +); From d3b21bb3409abf74bdd389b5de9cd5b296e13d37 Mon Sep 17 00:00:00 2001 From: Bailey Pearson Date: Tue, 11 Jan 2022 15:07:45 -0500 Subject: [PATCH 03/14] Add circular type fix --- src/mongo_types.ts | 16 +++++++- .../community/collection/findX.test-d.ts | 38 +++++++++++++++++++ 2 files changed, 53 insertions(+), 1 deletion(-) diff --git a/src/mongo_types.ts b/src/mongo_types.ts index 36fc7724f8..125cc31047 100644 --- a/src/mongo_types.ts +++ b/src/mongo_types.ts @@ -479,6 +479,20 @@ export type PropertyType = string extends Propert : unknown : unknown; +/** + * @public + * + * Helper type for NestedPaths to prevent circular reference errors. + * + * This helper type checks if the key is an optional property of the same type + * as the parent type. If so, rather than recursively call `NestedPaths`, we + * return the key type. This prevents the TS compiler from realizing we're + * looking at circular type references + */ +export type PseudoRecurseOnObject = Type[Key] extends Type | undefined + ? [Key] + : Type[Key]; + // We dont't support nested circular references /** @public */ export type NestedPaths = Type extends @@ -499,6 +513,6 @@ export type NestedPaths = Type extends : // eslint-disable-next-line @typescript-eslint/ban-types Type extends object ? { - [Key in Extract]: [Key, ...NestedPaths]; + [Key in Extract]: [Key, ...NestedPaths>]; }[Extract] : []; diff --git a/test/types/community/collection/findX.test-d.ts b/test/types/community/collection/findX.test-d.ts index 463ba1518e..e4f4f1d0fc 100644 --- a/test/types/community/collection/findX.test-d.ts +++ b/test/types/community/collection/findX.test-d.ts @@ -348,3 +348,41 @@ expectType | null>( expectType | null>( (await coll.findOneAndDelete({ a: 3 }, { projection: { _id: 0 } })).value ); + +interface RecursiveSchema { + name: RecursiveSchema; + age: number; +} + +declare const collection3: Collection; + +collection3.find({ + name: { + name: { + age: 23 + } + } +}); + +collection3.find({ + age: 23 +}); + +interface RecursiveOptionalSchema { + name?: RecursiveOptionalSchema; + age: number; +} + +declare const collection4: Collection; + +collection4.find({ + name: { + name: { + age: 23 + } + } +}); + +collection4.find({ + age: 23 +}); From bc09800c4e6e891d07ff9e78dc1c75a46977a90e Mon Sep 17 00:00:00 2001 From: Bailey Pearson Date: Tue, 11 Jan 2022 15:11:29 -0500 Subject: [PATCH 04/14] Export helper type --- src/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/index.ts b/src/index.ts index eb7cc75f97..f127c74ce7 100644 --- a/src/index.ts +++ b/src/index.ts @@ -299,6 +299,7 @@ export type { Projection, ProjectionOperators, PropertyType, + PseudoRecurseOnObject, PullAllOperator, PullOperator, PushOperator, From 19ef058725eeaa2ffc03413311d07c2b4e6c5977 Mon Sep 17 00:00:00 2001 From: Bailey Pearson Date: Wed, 12 Jan 2022 11:56:08 -0500 Subject: [PATCH 05/14] Add rough support for self-referential types Co-authored-by: Neal Beeken --- src/index.ts | 1 - src/mongo_types.ts | 19 +--- .../community/collection/findX.test-d.ts | 92 ++++++++++++++++++- 3 files changed, 96 insertions(+), 16 deletions(-) diff --git a/src/index.ts b/src/index.ts index f127c74ce7..eb7cc75f97 100644 --- a/src/index.ts +++ b/src/index.ts @@ -299,7 +299,6 @@ export type { Projection, ProjectionOperators, PropertyType, - PseudoRecurseOnObject, PullAllOperator, PullOperator, PushOperator, diff --git a/src/mongo_types.ts b/src/mongo_types.ts index 125cc31047..6706b907f2 100644 --- a/src/mongo_types.ts +++ b/src/mongo_types.ts @@ -481,20 +481,9 @@ export type PropertyType = string extends Propert /** * @public - * - * Helper type for NestedPaths to prevent circular reference errors. - * - * This helper type checks if the key is an optional property of the same type - * as the parent type. If so, rather than recursively call `NestedPaths`, we - * return the key type. This prevents the TS compiler from realizing we're - * looking at circular type references + * returns tuple of strings (keys to be joined on '.') that represent every path into a schema + * https://docs.mongodb.com/manual/tutorial/query-embedded-documents/ */ -export type PseudoRecurseOnObject = Type[Key] extends Type | undefined - ? [Key] - : Type[Key]; - -// We dont't support nested circular references -/** @public */ export type NestedPaths = Type extends | string | number @@ -513,6 +502,8 @@ export type NestedPaths = Type extends : // eslint-disable-next-line @typescript-eslint/ban-types Type extends object ? { - [Key in Extract]: [Key, ...NestedPaths>]; + [Key in Extract]: Type[Key] extends Type | ReadonlyArray | undefined + ? [Key] + : [Key, ...NestedPaths]; }[Extract] : []; diff --git a/test/types/community/collection/findX.test-d.ts b/test/types/community/collection/findX.test-d.ts index e4f4f1d0fc..5424565dea 100644 --- a/test/types/community/collection/findX.test-d.ts +++ b/test/types/community/collection/findX.test-d.ts @@ -1,4 +1,4 @@ -import { expectAssignable, expectNotType, expectType } from 'tsd'; +import { expectAssignable, expectError, expectNotType, expectType } from 'tsd'; import type { Filter, Projection, ProjectionOperators } from '../../../../src'; import { @@ -349,6 +349,32 @@ expectType | null>( (await coll.findOneAndDelete({ a: 3 }, { projection: { _id: 0 } })).value ); +interface A { + b: B; +} + +interface B { + a: A; +} + +declare const mutuallyRecursive: Collection; +expectError(() => mutuallyRecursive.find({})); +// expectError( +// mutuallyRecursive.find({ +// 'b.a.b': {} +// }) +// ); + +interface RecursiveButNotReally { + a: { a: number; b: string }; + b: string; +} + +declare const recursiveButNotReallyColl: Collection; +recursiveButNotReallyColl.find({ + 'a.a': 2 +}); + interface RecursiveSchema { name: RecursiveSchema; age: number; @@ -386,3 +412,67 @@ collection4.find({ collection4.find({ age: 23 }); + +// we don't support recursive union types currently +interface Node { + next: Node | null; +} + +declare const nodeCollection: Collection; + +expectError( + nodeCollection.find({ + next: { + next: null + } + }) +); + +interface MongoStrings { + projectId: number; + branches: Branch[]; +} + +interface Branch { + id: number; + name: string; + title?: string; + directories: Directory[]; +} + +interface Directory { + id: number; + name: string; + title?: string; + branchId: number; + files: Directory[]; +} + +declare const coll5: Collection; +expectError( + coll5.findOne({ + 'branches.0.id': 'hello' + }) +); + +expectError( + coll5.findOne({ + 'branches.0.directories.0.id': 'hello' + }) +); + +// type safety breaks after the first +// level of nested types +coll5.findOne({ + 'branches.0.directories.0.files.0.id': 'hello' +}); + +expectError( + coll5.find({ + branches: [ + { + id: 'asdf' + } + ] + }) +); From 879d909b86cafcdcd2952c17f600b2a495c9dad2 Mon Sep 17 00:00:00 2001 From: Bailey Pearson Date: Wed, 12 Jan 2022 13:14:20 -0500 Subject: [PATCH 06/14] staging temporarily --- .../community/collection/findX.test-d.ts | 38 +++++++++++++++++-- 1 file changed, 34 insertions(+), 4 deletions(-) diff --git a/test/types/community/collection/findX.test-d.ts b/test/types/community/collection/findX.test-d.ts index 5424565dea..1920afff2d 100644 --- a/test/types/community/collection/findX.test-d.ts +++ b/test/types/community/collection/findX.test-d.ts @@ -358,7 +358,8 @@ interface B { } declare const mutuallyRecursive: Collection; -expectError(() => mutuallyRecursive.find({})); +//@ts-expect-error +mutuallyRecursive.find({}); // expectError( // mutuallyRecursive.find({ // 'b.a.b': {} @@ -420,14 +421,33 @@ interface Node { declare const nodeCollection: Collection; +// vscode doesn't pick up the error, but the compiler does when running the tests +// @ts-expect-error +nodeCollection.find({ + next: null +}); + +nodeCollection.find({ + next: { + next: null + } +}); + expectError( nodeCollection.find({ - next: { - next: null - } + next: 'asdf' }) ); +// vscode flags this as an error for me but the test fails +expectError( + nodeCollection.find({ + 'next.next': 'asdf' + }) +); + +nodeCollection.find({ 'next.next.next': null }); + interface MongoStrings { projectId: number; branches: Branch[]; @@ -467,6 +487,7 @@ coll5.findOne({ 'branches.0.directories.0.files.0.id': 'hello' }); +// I'd expect this to error too? expectError( coll5.find({ branches: [ @@ -476,3 +497,12 @@ expectError( ] }) ); + +interface Test { + a: string; + b: number; +} + +declare const c: Collection; + +expectError(c.find({ a: 'asdf' })); From dbb0063a787d2e9fad72602ae922f710d1c0e1d4 Mon Sep 17 00:00:00 2001 From: Bailey Pearson Date: Wed, 12 Jan 2022 13:17:11 -0500 Subject: [PATCH 07/14] Add quick comment --- test/types/community/collection/findX.test-d.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/test/types/community/collection/findX.test-d.ts b/test/types/community/collection/findX.test-d.ts index 1920afff2d..67900f9f5c 100644 --- a/test/types/community/collection/findX.test-d.ts +++ b/test/types/community/collection/findX.test-d.ts @@ -505,4 +505,5 @@ interface Test { declare const c: Collection; +// this should fail, but doesn't expectError(c.find({ a: 'asdf' })); From 18a9232da537c7da91d1140724bac61e5f42b544 Mon Sep 17 00:00:00 2001 From: Bailey Pearson Date: Wed, 12 Jan 2022 13:31:49 -0500 Subject: [PATCH 08/14] Stage for swap branches --- .../community/collection/findX.test-d.ts | 43 ++++++++++++++----- 1 file changed, 32 insertions(+), 11 deletions(-) diff --git a/test/types/community/collection/findX.test-d.ts b/test/types/community/collection/findX.test-d.ts index 67900f9f5c..13e593a4e0 100644 --- a/test/types/community/collection/findX.test-d.ts +++ b/test/types/community/collection/findX.test-d.ts @@ -487,23 +487,44 @@ coll5.findOne({ 'branches.0.directories.0.files.0.id': 'hello' }); -// I'd expect this to error too? +// type inference on objects only works at the top level +coll5.findOne({ + branches: [ + { + id: 'asdf' + } + ] +}); + expectError( - coll5.find({ - branches: [ - { - id: 'asdf' - } - ] + coll5.findOne({ + projectId: 'asdf' }) ); +expectError( + coll5.findOne({ + branches: 'not array' + }) +); interface Test { - a: string; - b: number; + a: { + b: string; + }; } declare const c: Collection; +expectError( + c.findOne({ + a: { + b: 3 + } + }) +); -// this should fail, but doesn't -expectError(c.find({ a: 'asdf' })); +// this works +expectError( + c.findOne({ + 'a.b': 3 + }) +); From 85e42615b5bec11b1f4def2a0af8fc605ee4c598 Mon Sep 17 00:00:00 2001 From: Bailey Pearson Date: Wed, 12 Jan 2022 13:51:20 -0500 Subject: [PATCH 09/14] sort through tests --- .../community/collection/findX.test-d.ts | 94 +++++++++---------- 1 file changed, 43 insertions(+), 51 deletions(-) diff --git a/test/types/community/collection/findX.test-d.ts b/test/types/community/collection/findX.test-d.ts index 13e593a4e0..fcf35b7aec 100644 --- a/test/types/community/collection/findX.test-d.ts +++ b/test/types/community/collection/findX.test-d.ts @@ -349,6 +349,9 @@ expectType | null>( (await coll.findOneAndDelete({ a: 3 }, { projection: { _id: 0 } })).value ); +/** + * mutually recursive types are not supported and will not get type safety + */ interface A { b: B; } @@ -360,29 +363,38 @@ interface B { declare const mutuallyRecursive: Collection; //@ts-expect-error mutuallyRecursive.find({}); -// expectError( -// mutuallyRecursive.find({ -// 'b.a.b': {} -// }) -// ); +mutuallyRecursive.find({ + b: {} +}); +/** + * types that are not recursive in name but are recursive in structure are + * still supported + */ interface RecursiveButNotReally { a: { a: number; b: string }; b: string; } declare const recursiveButNotReallyColl: Collection; +expectError( + recursiveButNotReallyColl.find({ + 'a.a': 'asdf' + }) +); recursiveButNotReallyColl.find({ 'a.a': 2 }); +/** + * recursive schemas are now supported, but with limited type checking support + */ interface RecursiveSchema { name: RecursiveSchema; age: number; } declare const collection3: Collection; - collection3.find({ name: { name: { @@ -395,6 +407,10 @@ collection3.find({ age: 23 }); +/** + * Recursive optional schemas are also supported with the same capabilities as + * standard recursive schemas + */ interface RecursiveOptionalSchema { name?: RecursiveOptionalSchema; age: number; @@ -414,43 +430,40 @@ collection4.find({ age: 23 }); -// we don't support recursive union types currently +/** + * recursive union types are not supported + */ interface Node { next: Node | null; } declare const nodeCollection: Collection; -// vscode doesn't pick up the error, but the compiler does when running the tests +// circular type error is thrown // @ts-expect-error nodeCollection.find({ next: null }); nodeCollection.find({ - next: { - next: null - } + next: 'asdf' }); -expectError( - nodeCollection.find({ - next: 'asdf' - }) -); - -// vscode flags this as an error for me but the test fails -expectError( - nodeCollection.find({ - 'next.next': 'asdf' - }) -); +nodeCollection.find({ + 'next.next': 'asdf' +}); -nodeCollection.find({ 'next.next.next': null }); +nodeCollection.find({ 'next.next.next': 'yoohoo' }); +/** + * Recursive schemas with arrays are also supported + */ interface MongoStrings { projectId: number; branches: Branch[]; + twoLevelsDeep: { + name: string; + }; } interface Branch { @@ -487,7 +500,6 @@ coll5.findOne({ 'branches.0.directories.0.files.0.id': 'hello' }); -// type inference on objects only works at the top level coll5.findOne({ branches: [ { @@ -496,35 +508,15 @@ coll5.findOne({ ] }); +// type inference works on non-recursive properties but only at the top level expectError( coll5.findOne({ projectId: 'asdf' }) ); -expectError( - coll5.findOne({ - branches: 'not array' - }) -); -interface Test { - a: { - b: string; - }; -} - -declare const c: Collection; -expectError( - c.findOne({ - a: { - b: 3 - } - }) -); - -// this works -expectError( - c.findOne({ - 'a.b': 3 - }) -); +coll5.findOne({ + twoLevelsDeep: { + name: 3 + } +}); From 178018bbc6b9cf3d31697c9bfc2390e9fa41e6c1 Mon Sep 17 00:00:00 2001 From: Bailey Pearson Date: Thu, 13 Jan 2022 14:01:53 -0500 Subject: [PATCH 10/14] Recursive union types working --- src/mongo_types.ts | 4 +++- test/types/community/collection/findX.test-d.ts | 14 +++++++------- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/src/mongo_types.ts b/src/mongo_types.ts index 6706b907f2..acb77bf7e5 100644 --- a/src/mongo_types.ts +++ b/src/mongo_types.ts @@ -502,7 +502,9 @@ export type NestedPaths = Type extends : // eslint-disable-next-line @typescript-eslint/ban-types Type extends object ? { - [Key in Extract]: Type[Key] extends Type | ReadonlyArray | undefined + [Key in Extract]: Type[Key] extends Type | ReadonlyArray + ? [Key] + : Type extends Type[Key] ? [Key] : [Key, ...NestedPaths]; }[Extract] diff --git a/test/types/community/collection/findX.test-d.ts b/test/types/community/collection/findX.test-d.ts index fcf35b7aec..7ae7730660 100644 --- a/test/types/community/collection/findX.test-d.ts +++ b/test/types/community/collection/findX.test-d.ts @@ -431,7 +431,7 @@ collection4.find({ }); /** - * recursive union types are not supported + * recursive union types are supported */ interface Node { next: Node | null; @@ -439,15 +439,15 @@ interface Node { declare const nodeCollection: Collection; -// circular type error is thrown -// @ts-expect-error nodeCollection.find({ next: null }); -nodeCollection.find({ - next: 'asdf' -}); +expectError( + nodeCollection.find({ + next: 'asdf' + }) +); nodeCollection.find({ 'next.next': 'asdf' @@ -508,7 +508,7 @@ coll5.findOne({ ] }); -// type inference works on non-recursive properties but only at the top level +// type inference works on properties but only at the top level expectError( coll5.findOne({ projectId: 'asdf' From 4fdb78fa77ba24e96a0a02699ac951d49d4296a1 Mon Sep 17 00:00:00 2001 From: Bailey Pearson Date: Thu, 13 Jan 2022 14:15:35 -0500 Subject: [PATCH 11/14] Add support for recursive union array types --- src/mongo_types.ts | 8 +++++++- test/types/community/collection/findX.test-d.ts | 2 +- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/mongo_types.ts b/src/mongo_types.ts index acb77bf7e5..9fd7ffed17 100644 --- a/src/mongo_types.ts +++ b/src/mongo_types.ts @@ -502,10 +502,16 @@ export type NestedPaths = Type extends : // eslint-disable-next-line @typescript-eslint/ban-types Type extends object ? { - [Key in Extract]: Type[Key] extends Type | ReadonlyArray + [Key in Extract]: Type[Key] extends Type ? [Key] : Type extends Type[Key] ? [Key] + : Type[Key] extends ReadonlyArray + ? Type extends ArrayType + ? [Key] + : ArrayType extends Type + ? [Key] + : [Key, ...NestedPaths] : [Key, ...NestedPaths]; }[Extract] : []; diff --git a/test/types/community/collection/findX.test-d.ts b/test/types/community/collection/findX.test-d.ts index 7ae7730660..7bd9d40088 100644 --- a/test/types/community/collection/findX.test-d.ts +++ b/test/types/community/collection/findX.test-d.ts @@ -478,7 +478,7 @@ interface Directory { name: string; title?: string; branchId: number; - files: Directory[]; + files: (number | Directory)[]; } declare const coll5: Collection; From 85ffbfd31da1376d8829144ef36622e4d5a292d0 Mon Sep 17 00:00:00 2001 From: Bailey Pearson Date: Thu, 13 Jan 2022 14:22:20 -0500 Subject: [PATCH 12/14] Break recursive type tests into separate file --- .../findX-recursive-types.test-d.ts | 175 ++++++++++++++++++ .../community/collection/findX.test-d.ts | 172 ----------------- 2 files changed, 175 insertions(+), 172 deletions(-) create mode 100644 test/types/community/collection/findX-recursive-types.test-d.ts diff --git a/test/types/community/collection/findX-recursive-types.test-d.ts b/test/types/community/collection/findX-recursive-types.test-d.ts new file mode 100644 index 0000000000..96c4e095cc --- /dev/null +++ b/test/types/community/collection/findX-recursive-types.test-d.ts @@ -0,0 +1,175 @@ +import { expectError } from 'tsd'; + +import type { Collection } from '../../../../src'; + +/** + * mutually recursive types are not supported and will not get type safety + */ +interface A { + b: B; +} + +interface B { + a: A; +} + +declare const mutuallyRecursive: Collection; +//@ts-expect-error +mutuallyRecursive.find({}); +mutuallyRecursive.find({ + b: {} +}); + +/** + * types that are not recursive in name but are recursive in structure are + * still supported + */ +interface RecursiveButNotReally { + a: { a: number; b: string }; + b: string; +} + +declare const recursiveButNotReallyCollection: Collection; +expectError( + recursiveButNotReallyCollection.find({ + 'a.a': 'asdf' + }) +); +recursiveButNotReallyCollection.find({ + 'a.a': 2 +}); + +/** + * recursive schemas are now supported, but with limited type checking support + */ +interface RecursiveSchema { + name: RecursiveSchema; + age: number; +} + +declare const recursiveCollection: Collection; +recursiveCollection.find({ + name: { + name: { + age: 23 + } + } +}); + +recursiveCollection.find({ + age: 23 +}); + +/** + * Recursive optional schemas are also supported with the same capabilities as + * standard recursive schemas + */ +interface RecursiveOptionalSchema { + name?: RecursiveOptionalSchema; + age: number; +} + +declare const recursiveOptionalCollection: Collection; + +recursiveOptionalCollection.find({ + name: { + name: { + age: 23 + } + } +}); + +recursiveOptionalCollection.find({ + age: 23 +}); + +/** + * recursive union types are supported + */ +interface Node { + next: Node | null; +} + +declare const nodeCollection: Collection; + +nodeCollection.find({ + next: null +}); + +expectError( + nodeCollection.find({ + next: 'asdf' + }) +); + +nodeCollection.find({ + 'next.next': 'asdf' +}); + +nodeCollection.find({ 'next.next.next': 'yoohoo' }); + +/** + * Recursive schemas with arrays are also supported + */ +interface MongoStrings { + projectId: number; + branches: Branch[]; + twoLevelsDeep: { + name: string; + }; +} + +interface Branch { + id: number; + name: string; + title?: string; + directories: Directory[]; +} + +interface Directory { + id: number; + name: string; + title?: string; + branchId: number; + files: (number | Directory)[]; +} + +declare const recursiveSchemaWithArray: Collection; +expectError( + recursiveSchemaWithArray.findOne({ + 'branches.0.id': 'hello' + }) +); + +expectError( + recursiveSchemaWithArray.findOne({ + 'branches.0.directories.0.id': 'hello' + }) +); + +// type safety breaks after the first +// level of nested types +recursiveSchemaWithArray.findOne({ + 'branches.0.directories.0.files.0.id': 'hello' +}); + +recursiveSchemaWithArray.findOne({ + branches: [ + { + id: 'asdf' + } + ] +}); + +// type inference works on properties but only at the top level +expectError( + recursiveSchemaWithArray.findOne({ + projectId: 'asdf' + }) +); + +recursiveSchemaWithArray.findOne({ + twoLevelsDeep: { + name: 3 + } +}); diff --git a/test/types/community/collection/findX.test-d.ts b/test/types/community/collection/findX.test-d.ts index 7bd9d40088..26116f9fa3 100644 --- a/test/types/community/collection/findX.test-d.ts +++ b/test/types/community/collection/findX.test-d.ts @@ -348,175 +348,3 @@ expectType | null>( expectType | null>( (await coll.findOneAndDelete({ a: 3 }, { projection: { _id: 0 } })).value ); - -/** - * mutually recursive types are not supported and will not get type safety - */ -interface A { - b: B; -} - -interface B { - a: A; -} - -declare const mutuallyRecursive: Collection; -//@ts-expect-error -mutuallyRecursive.find({}); -mutuallyRecursive.find({ - b: {} -}); - -/** - * types that are not recursive in name but are recursive in structure are - * still supported - */ -interface RecursiveButNotReally { - a: { a: number; b: string }; - b: string; -} - -declare const recursiveButNotReallyColl: Collection; -expectError( - recursiveButNotReallyColl.find({ - 'a.a': 'asdf' - }) -); -recursiveButNotReallyColl.find({ - 'a.a': 2 -}); - -/** - * recursive schemas are now supported, but with limited type checking support - */ -interface RecursiveSchema { - name: RecursiveSchema; - age: number; -} - -declare const collection3: Collection; -collection3.find({ - name: { - name: { - age: 23 - } - } -}); - -collection3.find({ - age: 23 -}); - -/** - * Recursive optional schemas are also supported with the same capabilities as - * standard recursive schemas - */ -interface RecursiveOptionalSchema { - name?: RecursiveOptionalSchema; - age: number; -} - -declare const collection4: Collection; - -collection4.find({ - name: { - name: { - age: 23 - } - } -}); - -collection4.find({ - age: 23 -}); - -/** - * recursive union types are supported - */ -interface Node { - next: Node | null; -} - -declare const nodeCollection: Collection; - -nodeCollection.find({ - next: null -}); - -expectError( - nodeCollection.find({ - next: 'asdf' - }) -); - -nodeCollection.find({ - 'next.next': 'asdf' -}); - -nodeCollection.find({ 'next.next.next': 'yoohoo' }); - -/** - * Recursive schemas with arrays are also supported - */ -interface MongoStrings { - projectId: number; - branches: Branch[]; - twoLevelsDeep: { - name: string; - }; -} - -interface Branch { - id: number; - name: string; - title?: string; - directories: Directory[]; -} - -interface Directory { - id: number; - name: string; - title?: string; - branchId: number; - files: (number | Directory)[]; -} - -declare const coll5: Collection; -expectError( - coll5.findOne({ - 'branches.0.id': 'hello' - }) -); - -expectError( - coll5.findOne({ - 'branches.0.directories.0.id': 'hello' - }) -); - -// type safety breaks after the first -// level of nested types -coll5.findOne({ - 'branches.0.directories.0.files.0.id': 'hello' -}); - -coll5.findOne({ - branches: [ - { - id: 'asdf' - } - ] -}); - -// type inference works on properties but only at the top level -expectError( - coll5.findOne({ - projectId: 'asdf' - }) -); - -coll5.findOne({ - twoLevelsDeep: { - name: 3 - } -}); From 14c25caaafe2e75400dc4e50c24e3618f1e888cf Mon Sep 17 00:00:00 2001 From: Bailey Pearson Date: Thu, 13 Jan 2022 16:28:03 -0500 Subject: [PATCH 13/14] Add comments explaining the logic in recursive schema code --- src/mongo_types.ts | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/src/mongo_types.ts b/src/mongo_types.ts index 9fd7ffed17..83bf11ad6a 100644 --- a/src/mongo_types.ts +++ b/src/mongo_types.ts @@ -502,16 +502,21 @@ export type NestedPaths = Type extends : // eslint-disable-next-line @typescript-eslint/ban-types Type extends object ? { - [Key in Extract]: Type[Key] extends Type + [Key in Extract]: Type[Key] extends Type // type of value extends the parent ? [Key] - : Type extends Type[Key] + : // for a recursive union type, the child will never extend the parent type. + // but the parent will still extend the child + Type extends Type[Key] ? [Key] - : Type[Key] extends ReadonlyArray - ? Type extends ArrayType - ? [Key] - : ArrayType extends Type - ? [Key] - : [Key, ...NestedPaths] - : [Key, ...NestedPaths]; + : Type[Key] extends ReadonlyArray // handling recursive types with arrays + ? Type extends ArrayType // is the type of the parent the same as the type of the array? + ? [Key] // yes, it's a recursive array type + : // for unions, the child type extends the parent + ArrayType extends Type + ? [Key] // we have a recursive array union + : // child is an array, but it's not a recursive array + [Key, ...NestedPaths] + : // child is not structured the same as the parent + [Key, ...NestedPaths]; }[Extract] : []; From 50c0f28a64e6fa68a1d66614b3c4d3f6ff71c4f2 Mon Sep 17 00:00:00 2001 From: Bailey Pearson Date: Fri, 14 Jan 2022 10:36:57 -0500 Subject: [PATCH 14/14] remove unused import to fix linting --- test/types/community/collection/findX.test-d.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/types/community/collection/findX.test-d.ts b/test/types/community/collection/findX.test-d.ts index 26116f9fa3..463ba1518e 100644 --- a/test/types/community/collection/findX.test-d.ts +++ b/test/types/community/collection/findX.test-d.ts @@ -1,4 +1,4 @@ -import { expectAssignable, expectError, expectNotType, expectType } from 'tsd'; +import { expectAssignable, expectNotType, expectType } from 'tsd'; import type { Filter, Projection, ProjectionOperators } from '../../../../src'; import {