Skip to content

Commit 26bce4a

Browse files
authoredOct 13, 2022
feat(NODE-3875): support recursive schema types (#3433)
1 parent a7dab96 commit 26bce4a

7 files changed

+772
-198
lines changed
 

‎src/mongo_types.ts

+26-17
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ export type WithoutId<TSchema> = Omit<TSchema, '_id'>;
6868
export type Filter<TSchema> =
6969
| Partial<TSchema>
7070
| ({
71-
[Property in Join<NestedPaths<WithId<TSchema>>, '.'>]?: Condition<
71+
[Property in Join<NestedPaths<WithId<TSchema>, []>, '.'>]?: Condition<
7272
PropertyType<WithId<TSchema>, Property>
7373
>;
7474
} & RootFilterOperators<WithId<TSchema>>);
@@ -263,7 +263,7 @@ export type OnlyFieldsOfType<TSchema, FieldType = any, AssignableType = FieldTyp
263263
/** @public */
264264
export type MatchKeysAndValues<TSchema> = Readonly<
265265
{
266-
[Property in Join<NestedPaths<TSchema>, '.'>]?: PropertyType<TSchema, Property>;
266+
[Property in Join<NestedPaths<TSchema, []>, '.'>]?: PropertyType<TSchema, Property>;
267267
} & {
268268
[Property in `${NestedPathsOfType<TSchema, any[]>}.$${`[${string}]` | ''}`]?: ArrayElement<
269269
PropertyType<TSchema, Property extends `${infer Key}.$${string}` ? Key : never>
@@ -272,7 +272,7 @@ export type MatchKeysAndValues<TSchema> = Readonly<
272272
[Property in `${NestedPathsOfType<TSchema, Record<string, any>[]>}.$${
273273
| `[${string}]`
274274
| ''}.${string}`]?: any; // Could be further narrowed
275-
}
275+
} & Document
276276
>;
277277

278278
/** @public */
@@ -498,20 +498,29 @@ export type PropertyType<Type, Property extends string> = string extends Propert
498498
* @public
499499
* returns tuple of strings (keys to be joined on '.') that represent every path into a schema
500500
* https://docs.mongodb.com/manual/tutorial/query-embedded-documents/
501+
*
502+
* @remarks
503+
* Through testing we determined that a depth of 8 is safe for the typescript compiler
504+
* and provides reasonable compilation times. This number is otherwise not special and
505+
* should be changed if issues are found with this level of checking. Beyond this
506+
* depth any helpers that make use of NestedPaths should devolve to not asserting any
507+
* type safety on the input.
501508
*/
502-
export type NestedPaths<Type> = Type extends
503-
| string
504-
| number
505-
| boolean
506-
| Date
507-
| RegExp
508-
| Buffer
509-
| Uint8Array
510-
| ((...args: any[]) => any)
511-
| { _bsontype: string }
509+
export type NestedPaths<Type, Depth extends number[]> = Depth['length'] extends 8
510+
? []
511+
: Type extends
512+
| string
513+
| number
514+
| boolean
515+
| Date
516+
| RegExp
517+
| Buffer
518+
| Uint8Array
519+
| ((...args: any[]) => any)
520+
| { _bsontype: string }
512521
? []
513522
: Type extends ReadonlyArray<infer ArrayType>
514-
? [] | [number, ...NestedPaths<ArrayType>]
523+
? [] | [number, ...NestedPaths<ArrayType, [...Depth, 1]>]
515524
: Type extends Map<string, any>
516525
? [string]
517526
: Type extends object
@@ -529,9 +538,9 @@ export type NestedPaths<Type> = Type extends
529538
ArrayType extends Type
530539
? [Key] // we have a recursive array union
531540
: // child is an array, but it's not a recursive array
532-
[Key, ...NestedPaths<Type[Key]>]
541+
[Key, ...NestedPaths<Type[Key], [...Depth, 1]>]
533542
: // child is not structured the same as the parent
534-
[Key, ...NestedPaths<Type[Key]>] | [Key];
543+
[Key, ...NestedPaths<Type[Key], [...Depth, 1]>] | [Key];
535544
}[Extract<keyof Type, string>]
536545
: [];
537546

@@ -542,7 +551,7 @@ export type NestedPaths<Type> = Type extends
542551
*/
543552
export type NestedPathsOfType<TSchema, Type> = KeysOfAType<
544553
{
545-
[Property in Join<NestedPaths<TSchema>, '.'>]: PropertyType<TSchema, Property>;
554+
[Property in Join<NestedPaths<TSchema, []>, '.'>]: PropertyType<TSchema, Property>;
546555
},
547556
Type
548557
>;

‎test/types/basic_schema.test-d.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { expectAssignable, expectNotAssignable, expectNotType, expectType } from 'tsd';
22

3-
import { ObjectId } from '../../src/bson';
3+
import { Document, ObjectId } from '../../src/bson';
44
import { Collection } from '../../src/collection';
55
import { Db } from '../../src/db';
66
import { MongoClient } from '../../src/mongo_client';
@@ -20,7 +20,7 @@ expectType<Collection<ACounterWithId>>(new Collection<ACounterWithId>(db, ''));
2020
////////////////////////////////////////////////////////////////////////////////////////////////////
2121
// Simple Schema that does not define an _id
2222
// With _id
23-
type InsertOneArgOf<S> = Parameters<Collection<S>['insertOne']>[0];
23+
type InsertOneArgOf<S extends Document> = Parameters<Collection<S>['insertOne']>[0];
2424
expectAssignable<InsertOneArgOf<ACounter>>({ _id: new ObjectId(), a: 3 });
2525
// Without _id
2626
expectAssignable<InsertOneArgOf<ACounter>>({ a: 3 });

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

-175
This file was deleted.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,315 @@
1+
import { expectAssignable, expectError, expectNotAssignable, expectNotType } from 'tsd';
2+
3+
import type { Collection, Filter, UpdateFilter } from '../../../../src';
4+
5+
/**
6+
* mutually recursive types are not supported and will not get type safety
7+
*/
8+
interface Author {
9+
name: string;
10+
bestBook: Book;
11+
}
12+
13+
interface Book {
14+
title: string;
15+
author: Author;
16+
}
17+
18+
expectAssignable<Filter<Author>>({
19+
bestBook: {
20+
title: 'book title',
21+
author: {
22+
name: 'author name'
23+
}
24+
}
25+
});
26+
27+
// Check that devolving to Document after a certain recursive depth does not affect checking
28+
// cases where dot notation is not being used
29+
expectNotType<UpdateFilter<Author>>({
30+
$set: {
31+
bestBook: {
32+
title: 'a title',
33+
published: new Date(),
34+
author: {
35+
name: 23
36+
}
37+
}
38+
}
39+
});
40+
41+
//////////// Filter
42+
// Depth of 1 has type checking
43+
expectNotAssignable<Filter<Author>>({
44+
'bestBook.title': 23
45+
});
46+
// Depth of 2 has type checking
47+
expectNotAssignable<Filter<Author>>({
48+
'bestBook.author.name': 23
49+
});
50+
// Depth of 3 has type checking
51+
expectNotAssignable<Filter<Author>>({
52+
'bestBook.author.bestBook.title': 23
53+
});
54+
// Depth of 4 has type checking
55+
expectNotAssignable<Filter<Author>>({
56+
'bestBook.author.bestBook.author.name': 23
57+
});
58+
// Depth of 5 has type checking
59+
expectNotAssignable<Filter<Author>>({
60+
'bestBook.author.bestBook.author.bestBook.title': 23
61+
});
62+
// Depth of 6 has type checking
63+
expectNotAssignable<Filter<Author>>({
64+
'bestBook.author.bestBook.author.bestBook.author.name': 23
65+
});
66+
// Depth of 7 has type checking
67+
expectNotAssignable<Filter<Author>>({
68+
'bestBook.author.bestBook.author.bestBook.author.bestBook.title': 23
69+
});
70+
// Depth of 8 does **not** have type checking
71+
expectAssignable<Filter<Author>>({
72+
'bestBook.author.bestBook.author.bestBook.author.bestBook.author.name': 23
73+
});
74+
75+
//////////// UpdateFilter
76+
// Depth of 1 has type checking
77+
expectNotAssignable<UpdateFilter<Author>>({
78+
$set: {
79+
'bestBook.title': 23
80+
}
81+
});
82+
// Depth of 2 has type checking
83+
expectNotAssignable<UpdateFilter<Author>>({
84+
$set: {
85+
'bestBook.author.name': 23
86+
}
87+
});
88+
// Depth of 3 has type checking
89+
expectNotAssignable<UpdateFilter<Author>>({
90+
$set: {
91+
'bestBook.author.bestBook.title': 23
92+
}
93+
});
94+
// Depth of 4 has type checking
95+
expectNotAssignable<UpdateFilter<Author>>({
96+
$set: {
97+
'bestBook.author.bestBook.author.name': 23
98+
}
99+
});
100+
// Depth of 5 has type checking
101+
expectNotAssignable<UpdateFilter<Author>>({
102+
$set: {
103+
'bestBook.author.bestBook.author.bestBook.title': 23
104+
}
105+
});
106+
// Depth of 6 has type checking
107+
expectNotAssignable<UpdateFilter<Author>>({
108+
$set: {
109+
'bestBook.author.bestBook.author.bestBook.author.name': 23
110+
}
111+
});
112+
// Depth of 7 has type checking
113+
expectNotAssignable<UpdateFilter<Author>>({
114+
$set: {
115+
'bestBook.author.bestBook.author.bestBook.author.bestBook.title': 23
116+
}
117+
});
118+
// Depth of 8 does **not** have type checking
119+
expectAssignable<UpdateFilter<Author>>({
120+
$set: {
121+
'bestBook.author.bestBook.author.bestBook.author.bestBook.author.name': 23
122+
}
123+
});
124+
125+
/**
126+
* types that are not recursive in name but are recursive in structure are
127+
* still supported
128+
*/
129+
interface RecursiveButNotReally {
130+
a: { a: number; b: string };
131+
b: string;
132+
}
133+
134+
declare const recursiveButNotReallyCollection: Collection<RecursiveButNotReally>;
135+
expectError(
136+
recursiveButNotReallyCollection.find({
137+
'a.a': 'asdf'
138+
})
139+
);
140+
recursiveButNotReallyCollection.find({
141+
'a.a': 2
142+
});
143+
144+
/**
145+
* recursive schemas are now supported, but with limited type checking support
146+
*/
147+
interface RecursiveSchema {
148+
name: RecursiveSchema;
149+
age: number;
150+
}
151+
152+
declare const recursiveCollection: Collection<RecursiveSchema>;
153+
recursiveCollection.find({
154+
name: {
155+
name: {
156+
age: 23
157+
}
158+
}
159+
});
160+
161+
recursiveCollection.find({
162+
age: 23
163+
});
164+
165+
/**
166+
* Recursive optional schemas are also supported with the same capabilities as
167+
* standard recursive schemas
168+
*/
169+
interface RecursiveOptionalSchema {
170+
name?: RecursiveOptionalSchema;
171+
age: number;
172+
}
173+
174+
declare const recursiveOptionalCollection: Collection<RecursiveOptionalSchema>;
175+
176+
recursiveOptionalCollection.find({
177+
name: {
178+
name: {
179+
age: 23
180+
}
181+
}
182+
});
183+
184+
recursiveOptionalCollection.find({
185+
age: 23
186+
});
187+
188+
/**
189+
* recursive union types are supported
190+
*/
191+
interface Node {
192+
next: Node | null;
193+
}
194+
195+
declare const nodeCollection: Collection<Node>;
196+
197+
nodeCollection.find({
198+
next: null
199+
});
200+
201+
expectError(
202+
nodeCollection.find({
203+
next: 'asdf'
204+
})
205+
);
206+
207+
nodeCollection.find({
208+
'next.next': 'asdf'
209+
});
210+
211+
nodeCollection.find({ 'next.next.next': 'yoohoo' });
212+
213+
/**
214+
* Recursive schemas with arrays are also supported
215+
*/
216+
interface MongoStrings {
217+
projectId: number;
218+
branches: Branch[];
219+
twoLevelsDeep: {
220+
name: string;
221+
};
222+
}
223+
224+
interface Branch {
225+
id: number;
226+
name: string;
227+
title?: string;
228+
directories: Directory[];
229+
}
230+
231+
interface Directory {
232+
id: number;
233+
name: string;
234+
title?: string;
235+
branchId: number;
236+
files: (number | Directory)[];
237+
}
238+
239+
declare const recursiveSchemaWithArray: Collection<MongoStrings>;
240+
expectError(
241+
recursiveSchemaWithArray.findOne({
242+
'branches.0.id': 'hello'
243+
})
244+
);
245+
246+
expectError(
247+
recursiveSchemaWithArray.findOne({
248+
'branches.0.directories.0.id': 'hello'
249+
})
250+
);
251+
252+
// type safety breaks after the first
253+
// level of nested types
254+
recursiveSchemaWithArray.findOne({
255+
'branches.0.directories.0.files.0.id': 'hello'
256+
});
257+
258+
recursiveSchemaWithArray.findOne({
259+
branches: [
260+
{
261+
id: 'asdf'
262+
}
263+
]
264+
});
265+
266+
// type inference works on properties but only at the top level
267+
expectError(
268+
recursiveSchemaWithArray.findOne({
269+
projectId: 'asdf'
270+
})
271+
);
272+
273+
recursiveSchemaWithArray.findOne({
274+
twoLevelsDeep: {
275+
name: 3
276+
}
277+
});
278+
279+
// Modeling A -> B -> C -> D -> A recursive type
280+
type A = {
281+
name: string;
282+
b: B;
283+
};
284+
285+
type B = {
286+
name: string;
287+
c: C;
288+
};
289+
290+
type C = {
291+
name: string;
292+
d: D;
293+
};
294+
295+
type D = {
296+
name: string;
297+
a: A;
298+
};
299+
300+
expectAssignable<Filter<A>>({
301+
'b.c.d.a.b.c.d.a.b.name': 'a'
302+
});
303+
304+
// Beyond the depth supported, there is no type checking
305+
expectAssignable<Filter<A>>({
306+
'b.c.d.a.b.c.d.a.b.c.name': 3
307+
});
308+
309+
expectAssignable<UpdateFilter<A>>({
310+
$set: { 'b.c.d.a.b.c.d.a.b.name': 'a' }
311+
});
312+
313+
expectAssignable<UpdateFilter<A>>({
314+
$set: { 'b.c.d.a.b.c.d.a.b.c.name': 'a' }
315+
});

‎test/types/community/collection/updateX.test-d.ts

+8-2
Original file line numberDiff line numberDiff line change
@@ -221,7 +221,10 @@ expectError<UpdateFilter<TestModel>>({
221221
$set: { 'subInterfaceField.nestedObject': { a: 1, b: '2' } }
222222
});
223223
expectError(buildUpdateFilter({ $set: { 'subInterfaceField.field2': 2 } }));
224-
expectError(buildUpdateFilter({ $set: { 'unknown.field': null } }));
224+
225+
// NODE-3875 introduced intersection with Document to the MatchKeysAndValues so this no longer errors
226+
expectAssignable<UpdateFilter<TestModel>>({ $set: { 'unknown.field': null } });
227+
225228
expectAssignable<UpdateFilter<TestModel>>({ $set: { 'numberArray.$': 40 } });
226229
expectAssignable<UpdateFilter<TestModel>>({ $set: { 'numberArray.$[bla]': 40 } });
227230
expectAssignable<UpdateFilter<TestModel>>({ $set: { 'numberArray.$[]': 1000.2 } });
@@ -241,7 +244,10 @@ expectAssignable<UpdateFilter<TestModel>>({ $setOnInsert: { stringField: 'a' } }
241244
expectError(buildUpdateFilter({ $setOnInsert: { stringField: 123 } }));
242245
expectAssignable<UpdateFilter<TestModel>>({ $setOnInsert: { 'subInterfaceField.field1': '2' } });
243246
expectError(buildUpdateFilter({ $setOnInsert: { 'subInterfaceField.field2': 2 } }));
244-
expectError(buildUpdateFilter({ $setOnInsert: { 'unknown.field': null } }));
247+
248+
// NODE-3875 introduced intersection with Document to the MatchKeysAndValues so this no longer errors
249+
expectAssignable<UpdateFilter<TestModel>>({ $setOnInsert: { 'unknown.field': null } });
250+
245251
expectAssignable<UpdateFilter<TestModel>>({ $setOnInsert: { 'numberArray.$': 40 } });
246252
expectAssignable<UpdateFilter<TestModel>>({ $setOnInsert: { 'numberArray.$[bla]': 40 } });
247253
expectAssignable<UpdateFilter<TestModel>>({ $setOnInsert: { 'numberArray.$[]': 1000.2 } });
+419
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,419 @@
1+
/*
2+
* The following type testing was adapted from @ermik
3+
* it is a reproduction of a slow down in typescript compilation
4+
* as well as issues with compiling recursive schemas
5+
* https://github.com/thinkalpha/node-mongodb-typechecking-performance
6+
*/
7+
8+
import type { UUID } from 'bson';
9+
10+
import type { Collection, Db } from '../../../src';
11+
12+
interface MomentInputObject {
13+
years?: number;
14+
year?: number;
15+
y?: number;
16+
17+
months?: number;
18+
month?: number;
19+
M?: number;
20+
21+
days?: number;
22+
day?: number;
23+
d?: number;
24+
25+
dates?: number;
26+
date?: number;
27+
D?: number;
28+
29+
hours?: number;
30+
hour?: number;
31+
h?: number;
32+
33+
minutes?: number;
34+
minute?: number;
35+
m?: number;
36+
37+
seconds?: number;
38+
second?: number;
39+
s?: number;
40+
41+
milliseconds?: number;
42+
millisecond?: number;
43+
ms?: number;
44+
}
45+
46+
interface MomentSetObject extends MomentInputObject {
47+
weekYears?: number;
48+
weekYear?: number;
49+
gg?: number;
50+
51+
isoWeekYears?: number;
52+
isoWeekYear?: number;
53+
GG?: number;
54+
55+
quarters?: number;
56+
quarter?: number;
57+
Q?: number;
58+
59+
weeks?: number;
60+
week?: number;
61+
w?: number;
62+
63+
isoWeeks?: number;
64+
isoWeek?: number;
65+
W?: number;
66+
67+
dayOfYears?: number;
68+
dayOfYear?: number;
69+
DDD?: number;
70+
71+
weekdays?: number;
72+
weekday?: number;
73+
e?: number;
74+
75+
isoWeekdays?: number;
76+
isoWeekday?: number;
77+
E?: number;
78+
}
79+
80+
export type Model = ModelBase;
81+
82+
export type SpecificModel = Model;
83+
84+
export interface ModelBase {
85+
another: AnotherSpecificModel;
86+
}
87+
88+
export interface AnotherSpecificModel {
89+
specifics: SpecificModel[];
90+
}
91+
92+
export interface CircularModel {
93+
_id: UUID;
94+
data: SpecificModel;
95+
}
96+
97+
export type SlowModelPatch = Partial<SlowModelBase>;
98+
99+
export interface SlowModelBase {
100+
prop1: string;
101+
prop2: string;
102+
prop3: Type1;
103+
prop5: AltUnit | null;
104+
prop6: string | null;
105+
prop7: string | null;
106+
prop8: string[];
107+
prop9: ExtraType2[];
108+
prop10: boolean;
109+
prop11: string | null;
110+
prop12: string[];
111+
prop13: ExtraType4 | null;
112+
prop14: ExtraType5 | null;
113+
prop15: ExtraType6 | null;
114+
prop16: string[];
115+
prop17: string | null;
116+
prop18: ExtraType7[];
117+
prop19: ExtraType8[];
118+
prop20: boolean;
119+
prop21: boolean;
120+
prop22: number;
121+
}
122+
123+
export declare type Type1 = AltType1 | ExtraType1;
124+
125+
export declare type AltType1 = Type1A | Type1B | Type1C;
126+
127+
export declare enum Type1A {
128+
Key1 = 'value1',
129+
Key2 = 'value2',
130+
Key3 = 'value3',
131+
Key4 = 'value4',
132+
Key5 = 'value5',
133+
Key6 = 'value6',
134+
Key7 = 'value7',
135+
Key8 = 'value8',
136+
Key9 = 'value9',
137+
Key10 = 'value10',
138+
Key11 = 'value11',
139+
Key12 = 'value12',
140+
Key13 = 'value13',
141+
Key14 = 'value14'
142+
}
143+
144+
export declare type Type1B =
145+
| {
146+
type: Type1A.Key1 | Type1A.Key2;
147+
valueKey1Key2?: boolean;
148+
}
149+
| {
150+
type: Type1A.Key12;
151+
valueKey12?: boolean;
152+
}
153+
| {
154+
type: Type1A.Key6;
155+
}
156+
| {
157+
type: Type1A.Key7;
158+
}
159+
| {
160+
type: Type1A.Key13;
161+
}
162+
| {
163+
type: Type1A.Key5;
164+
}
165+
| {
166+
type: Type1A.Key14;
167+
}
168+
| {
169+
type: Type1A.Key8;
170+
}
171+
| {
172+
type: Type1A.Key11;
173+
}
174+
| {
175+
type: Type1A.Key9;
176+
}
177+
| {
178+
type: Type1A.Key3;
179+
}
180+
| {
181+
type: Type1A.Key10;
182+
}
183+
| {
184+
type: Type1A.Key4;
185+
};
186+
187+
export declare type ExtraType1 = {
188+
index: number;
189+
};
190+
export declare type Type1C = {
191+
series: AltType1;
192+
default?: TimeType;
193+
};
194+
195+
export declare enum PeriodType {
196+
second = 's',
197+
minute = 'm',
198+
hour = 'h',
199+
day = 'd',
200+
week = 'W',
201+
month = 'Mo',
202+
quarter = 'Q',
203+
year = 'Y'
204+
}
205+
export declare const PeriodNames: readonly string[];
206+
export declare const PeriodNameMap: ReadonlyMap<PeriodType, string>;
207+
export declare const PeriodTypes: readonly PeriodType[];
208+
export declare type LengthType = LengthType2;
209+
export interface LengthType2 {
210+
value: number;
211+
period: PeriodType;
212+
}
213+
export declare type TimeType2 = null | {
214+
periods: LengthType2[];
215+
anchor: MomentSetObject | null;
216+
};
217+
export declare type TimeType1 = Date;
218+
export declare type TimeType3 = number;
219+
220+
export declare type TimeType = TimeType1 | TimeType2 | TimeType3;
221+
222+
export declare enum UnitType1 {
223+
unit1 = 'unit1',
224+
unit2 = 'unit2',
225+
unit3 = 'unit3',
226+
unit4 = 'unit4',
227+
unit5 = 'unit5'
228+
}
229+
export declare enum UnitType2 {
230+
unitA = 'unitA',
231+
unitB = 'unitB',
232+
unitC = 'unitC',
233+
unitD = 'unitD'
234+
}
235+
export declare type ExtraUnit = {
236+
index: number;
237+
};
238+
export declare type AltUnit = UnitType1 | UnitType2;
239+
240+
export interface ExtraType2 {
241+
prop23?: string;
242+
prop24?: string;
243+
prop25: AltType1;
244+
prop26?: AltUnit;
245+
prop27?: string;
246+
prop28: boolean;
247+
prop29?: ExtraType9;
248+
prop30?: ExtraType10;
249+
prop31?: ExtraType11;
250+
prop32?: ExtraType12;
251+
prop33: boolean;
252+
}
253+
254+
export type ExtraType9 = ExtraType6;
255+
256+
export enum UnitType3 {
257+
unit_ = 'unit_',
258+
unit__ = 'unit__',
259+
unit___ = 'unit___',
260+
unit____ = 'unit____',
261+
unit_____ = 'unit_____'
262+
}
263+
264+
export interface ExtraType10 {
265+
prop34?: UnitType3;
266+
prop35?: number;
267+
prop36?: number;
268+
prop37?: boolean;
269+
}
270+
271+
export interface ExtraType11 {
272+
prop38?: number;
273+
prop39?: boolean;
274+
prop40?: boolean;
275+
prop41?: boolean;
276+
}
277+
278+
export interface ExtraType12 {
279+
_id?: UUID;
280+
prop42: ExtraType13[];
281+
}
282+
283+
export interface ExtraType14 {
284+
prop42: TimeType;
285+
prop43?: TimeType | null;
286+
prop44?: LengthType2;
287+
}
288+
289+
export type ExtraType15 = string | number | boolean | ExtraType14 | TimeType | LengthType2;
290+
export type ExtraType16 = {
291+
prop45: string | number;
292+
prop46: ExtraType15;
293+
prop47: ExtraType15;
294+
prop48: string;
295+
};
296+
export type ExtraType13 = ExtraType15 | ExtraType16;
297+
298+
export interface ExtraType6 {
299+
prop49: SpecialType[];
300+
prop50: string;
301+
}
302+
303+
export interface SlowModel extends SlowModelBase {
304+
_id: UUID;
305+
}
306+
307+
export interface SpecialType extends SlowModel {
308+
prop51: string;
309+
}
310+
311+
export enum ExtraType17 {
312+
Key16 = 'key16',
313+
Key17 = 'key17'
314+
}
315+
316+
export interface IType1 {
317+
type: ExtraType17;
318+
}
319+
export interface Key17IType1 extends IType1 {
320+
type: ExtraType17.Key17;
321+
prop52: string;
322+
prop53?: string;
323+
prop54?: string;
324+
}
325+
export interface Key16IType1 extends IType1 {
326+
type: ExtraType17.Key16;
327+
prop55: string;
328+
prop56: string | null;
329+
}
330+
331+
export type ExtraType4 = Key17IType1 | Key16IType1;
332+
333+
export enum ExtraType18 {
334+
Key18 = 'key18',
335+
Key19 = 'key19'
336+
}
337+
338+
interface IType2 {
339+
type: ExtraType18;
340+
}
341+
export type ExtraType5 = Key18IType2 | Key19IType2;
342+
343+
export interface Key18IType2 extends IType2 {
344+
type: ExtraType18.Key18;
345+
prop57: string;
346+
prop58: string;
347+
prop59: boolean;
348+
prop60: string | null;
349+
prop61: boolean;
350+
prop62?: string;
351+
}
352+
export interface Key19IType2 extends IType2 {
353+
type: ExtraType18.Key19;
354+
prop63: string;
355+
prop64?: string;
356+
prop65: boolean;
357+
}
358+
359+
export type ExtraType19 = ExtraType15[] | ExtraType20;
360+
export interface ExtraType7<T extends ExtraType19 = ExtraType19> {
361+
id: UUID;
362+
prop66?: string;
363+
prop67: T;
364+
prop68?: ExtraType21;
365+
prop69?: ExtraType22;
366+
prop70?: string;
367+
}
368+
369+
export interface ExtraType20 {
370+
prop71: string;
371+
prop72: string;
372+
prop73: number;
373+
prop74: SpecialType[];
374+
}
375+
376+
export interface ExtraType22 {
377+
prop75: string;
378+
}
379+
380+
export interface ExtraType21 {
381+
prop76: string;
382+
prop77: string;
383+
prop78: boolean;
384+
prop79: boolean;
385+
prop80: string | null;
386+
}
387+
388+
export interface ExtraType8 {
389+
id: UUID;
390+
prop81: string;
391+
prop82: ExtraType15[];
392+
prop83: ExtraType15;
393+
}
394+
395+
export class ExampleImpl {
396+
private circularCol: Collection<CircularModel>;
397+
private slowCol: Collection<SlowModel>;
398+
399+
constructor(db: Db) {
400+
this.circularCol = db.collection('circular');
401+
this.slowCol = db.collection('slow');
402+
}
403+
404+
public method = async (): Promise<void> => {
405+
/*
406+
* This call shows the base issue with recursive types
407+
*/
408+
await this.circularCol.findOne({});
409+
};
410+
411+
public update = async (patch: SlowModelPatch, prop1: string): Promise<void> => {
412+
/*
413+
* This call shows the major issue with type inference causing
414+
* an infinite typechecking loop / major slowdown in TypeScript
415+
* compilation times
416+
*/
417+
await this.slowCol.updateOne({}, { $set: { ...patch, prop1 } });
418+
};
419+
}

‎test/types/union_schema.test-d.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
import { expectAssignable, expectError, expectNotAssignable, expectNotType, expectType } from 'tsd';
22

3-
import { ObjectId } from '../../src/bson';
3+
import { Document, ObjectId } from '../../src/bson';
44
import type { Collection } from '../../src/collection';
55
import type { WithId } from '../../src/mongo_types';
66

7-
type InsertOneFirstParam<Schema> = Parameters<Collection<Schema>['insertOne']>[0];
7+
type InsertOneFirstParam<Schema extends Document> = Parameters<Collection<Schema>['insertOne']>[0];
88

99
interface Circle {
1010
_id: ObjectId;

0 commit comments

Comments
 (0)
Please sign in to comment.