Skip to content

Commit dd5195a

Browse files
fix(NODE-3852,NODE-3854,NODE-3856): Misc typescript fixes for 4.3.1 (#3102)
1 parent 2adc7cd commit dd5195a

File tree

4 files changed

+253
-10
lines changed

4 files changed

+253
-10
lines changed

src/collection.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ import { WriteConcern, WriteConcernOptions } from './write_concern';
102102

103103
/** @public */
104104
export interface ModifyResult<TSchema = Document> {
105-
value: TSchema | null;
105+
value: WithId<TSchema> | null;
106106
lastErrorObject?: Document;
107107
ok: 0 | 1;
108108
}

src/mongo_types.ts

Lines changed: 28 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -65,11 +65,13 @@ export type EnhancedOmit<TRecordOrUnion, KeyUnion> = string extends keyof TRecor
6565
export type WithoutId<TSchema> = Omit<TSchema, '_id'>;
6666

6767
/** A MongoDB filter can be some portion of the schema or a set of operators @public */
68-
export type Filter<TSchema> = {
69-
[Property in Join<NestedPaths<WithId<TSchema>>, '.'>]?: Condition<
70-
PropertyType<WithId<TSchema>, Property>
71-
>;
72-
} & RootFilterOperators<WithId<TSchema>>;
68+
export type Filter<TSchema> =
69+
| Partial<TSchema>
70+
| ({
71+
[Property in Join<NestedPaths<WithId<TSchema>>, '.'>]?: Condition<
72+
PropertyType<WithId<TSchema>, Property>
73+
>;
74+
} & RootFilterOperators<WithId<TSchema>>);
7375

7476
/** @public */
7577
export type Condition<T> = AlternativeType<T> | FilterOperators<AlternativeType<T>>;
@@ -477,8 +479,11 @@ export type PropertyType<Type, Property extends string> = string extends Propert
477479
: unknown
478480
: unknown;
479481

480-
// We dont't support nested circular references
481-
/** @public */
482+
/**
483+
* @public
484+
* returns tuple of strings (keys to be joined on '.') that represent every path into a schema
485+
* https://docs.mongodb.com/manual/tutorial/query-embedded-documents/
486+
*/
482487
export type NestedPaths<Type> = Type extends
483488
| string
484489
| number
@@ -497,6 +502,21 @@ export type NestedPaths<Type> = Type extends
497502
: // eslint-disable-next-line @typescript-eslint/ban-types
498503
Type extends object
499504
? {
500-
[Key in Extract<keyof Type, string>]: [Key, ...NestedPaths<Type[Key]>];
505+
[Key in Extract<keyof Type, string>]: Type[Key] extends Type // type of value extends the parent
506+
? [Key]
507+
: // for a recursive union type, the child will never extend the parent type.
508+
// but the parent will still extend the child
509+
Type extends Type[Key]
510+
? [Key]
511+
: Type[Key] extends ReadonlyArray<infer ArrayType> // handling recursive types with arrays
512+
? Type extends ArrayType // is the type of the parent the same as the type of the array?
513+
? [Key] // yes, it's a recursive array type
514+
: // for unions, the child type extends the parent
515+
ArrayType extends Type
516+
? [Key] // we have a recursive array union
517+
: // child is an array, but it's not a recursive array
518+
[Key, ...NestedPaths<Type[Key]>]
519+
: // child is not structured the same as the parent
520+
[Key, ...NestedPaths<Type[Key]>];
501521
}[Extract<keyof Type, string>]
502522
: [];
Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
import { expectError } from 'tsd';
2+
3+
import type { Collection } from '../../../../src';
4+
5+
/**
6+
* mutually recursive types are not supported and will not get type safety
7+
*/
8+
interface A {
9+
b: B;
10+
}
11+
12+
interface B {
13+
a: A;
14+
}
15+
16+
declare const mutuallyRecursive: Collection<A>;
17+
//@ts-expect-error
18+
mutuallyRecursive.find({});
19+
mutuallyRecursive.find({
20+
b: {}
21+
});
22+
23+
/**
24+
* types that are not recursive in name but are recursive in structure are
25+
* still supported
26+
*/
27+
interface RecursiveButNotReally {
28+
a: { a: number; b: string };
29+
b: string;
30+
}
31+
32+
declare const recursiveButNotReallyCollection: Collection<RecursiveButNotReally>;
33+
expectError(
34+
recursiveButNotReallyCollection.find({
35+
'a.a': 'asdf'
36+
})
37+
);
38+
recursiveButNotReallyCollection.find({
39+
'a.a': 2
40+
});
41+
42+
/**
43+
* recursive schemas are now supported, but with limited type checking support
44+
*/
45+
interface RecursiveSchema {
46+
name: RecursiveSchema;
47+
age: number;
48+
}
49+
50+
declare const recursiveCollection: Collection<RecursiveSchema>;
51+
recursiveCollection.find({
52+
name: {
53+
name: {
54+
age: 23
55+
}
56+
}
57+
});
58+
59+
recursiveCollection.find({
60+
age: 23
61+
});
62+
63+
/**
64+
* Recursive optional schemas are also supported with the same capabilities as
65+
* standard recursive schemas
66+
*/
67+
interface RecursiveOptionalSchema {
68+
name?: RecursiveOptionalSchema;
69+
age: number;
70+
}
71+
72+
declare const recursiveOptionalCollection: Collection<RecursiveOptionalSchema>;
73+
74+
recursiveOptionalCollection.find({
75+
name: {
76+
name: {
77+
age: 23
78+
}
79+
}
80+
});
81+
82+
recursiveOptionalCollection.find({
83+
age: 23
84+
});
85+
86+
/**
87+
* recursive union types are supported
88+
*/
89+
interface Node {
90+
next: Node | null;
91+
}
92+
93+
declare const nodeCollection: Collection<Node>;
94+
95+
nodeCollection.find({
96+
next: null
97+
});
98+
99+
expectError(
100+
nodeCollection.find({
101+
next: 'asdf'
102+
})
103+
);
104+
105+
nodeCollection.find({
106+
'next.next': 'asdf'
107+
});
108+
109+
nodeCollection.find({ 'next.next.next': 'yoohoo' });
110+
111+
/**
112+
* Recursive schemas with arrays are also supported
113+
*/
114+
interface MongoStrings {
115+
projectId: number;
116+
branches: Branch[];
117+
twoLevelsDeep: {
118+
name: string;
119+
};
120+
}
121+
122+
interface Branch {
123+
id: number;
124+
name: string;
125+
title?: string;
126+
directories: Directory[];
127+
}
128+
129+
interface Directory {
130+
id: number;
131+
name: string;
132+
title?: string;
133+
branchId: number;
134+
files: (number | Directory)[];
135+
}
136+
137+
declare const recursiveSchemaWithArray: Collection<MongoStrings>;
138+
expectError(
139+
recursiveSchemaWithArray.findOne({
140+
'branches.0.id': 'hello'
141+
})
142+
);
143+
144+
expectError(
145+
recursiveSchemaWithArray.findOne({
146+
'branches.0.directories.0.id': 'hello'
147+
})
148+
);
149+
150+
// type safety breaks after the first
151+
// level of nested types
152+
recursiveSchemaWithArray.findOne({
153+
'branches.0.directories.0.files.0.id': 'hello'
154+
});
155+
156+
recursiveSchemaWithArray.findOne({
157+
branches: [
158+
{
159+
id: 'asdf'
160+
}
161+
]
162+
});
163+
164+
// type inference works on properties but only at the top level
165+
expectError(
166+
recursiveSchemaWithArray.findOne({
167+
projectId: 'asdf'
168+
})
169+
);
170+
171+
recursiveSchemaWithArray.findOne({
172+
twoLevelsDeep: {
173+
name: 3
174+
}
175+
});

test/types/community/collection/findX.test-d.ts

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { expectAssignable, expectNotType, expectType } from 'tsd';
22

3-
import type { Projection, ProjectionOperators } from '../../../../src';
3+
import type { Filter, Projection, ProjectionOperators } from '../../../../src';
44
import {
55
Collection,
66
Db,
@@ -300,3 +300,51 @@ expectAssignable<SchemaWithUserDefinedId | null>(await schemaWithUserDefinedId.f
300300
// should allow _id as a number
301301
await schemaWithUserDefinedId.findOne({ _id: 5 });
302302
await schemaWithUserDefinedId.find({ _id: 5 });
303+
304+
// We should be able to use a doc of type T as a filter object when performing findX operations
305+
interface Foo {
306+
a: string;
307+
}
308+
309+
const fooObj: Foo = {
310+
a: 'john doe'
311+
};
312+
313+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
314+
const fooFilter: Filter<Foo> = fooObj;
315+
316+
// Specifically test that arrays can be included as a part of an object
317+
// ensuring that a bug reported in https://jira.mongodb.org/browse/NODE-3856 is addressed
318+
interface FooWithArray {
319+
a: number[];
320+
}
321+
322+
const fooObjWithArray: FooWithArray = {
323+
a: [1, 2, 3, 4]
324+
};
325+
326+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
327+
const fooFilterWithArray: Filter<FooWithArray> = fooObjWithArray;
328+
329+
declare const coll: Collection<{ a: number; b: string }>;
330+
expectType<WithId<{ a: number; b: string }> | null>((await coll.findOneAndDelete({ a: 3 })).value);
331+
expectType<WithId<{ a: number; b: string }> | null>(
332+
(await coll.findOneAndReplace({ a: 3 }, { a: 5, b: 'new string' })).value
333+
);
334+
expectType<WithId<{ a: number; b: string }> | null>(
335+
(
336+
await coll.findOneAndUpdate(
337+
{ a: 3 },
338+
{
339+
$set: {
340+
a: 5
341+
}
342+
}
343+
)
344+
).value
345+
);
346+
347+
// projections do not change the return type - our typing doesn't support this
348+
expectType<WithId<{ a: number; b: string }> | null>(
349+
(await coll.findOneAndDelete({ a: 3 }, { projection: { _id: 0 } })).value
350+
);

0 commit comments

Comments
 (0)