Skip to content

Commit ec1a68f

Browse files
authored
fix(NODE-4513): type for nested objects in query & update (#3349)
1 parent 6a0e502 commit ec1a68f

File tree

3 files changed

+112
-82
lines changed

3 files changed

+112
-82
lines changed

src/index.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -321,7 +321,6 @@ export type {
321321
KeysOfOtherType,
322322
MatchKeysAndValues,
323323
NestedPaths,
324-
NestedPathsOfType,
325324
NonObjectIdLikeDocument,
326325
NotAcceptedFields,
327326
NumericType,

src/mongo_types.ts

Lines changed: 78 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -68,8 +68,8 @@ export type WithoutId<TSchema> = Omit<TSchema, '_id'>;
6868
export type Filter<TSchema> =
6969
| Partial<TSchema>
7070
| ({
71-
[Property in Join<NestedPaths<WithId<TSchema>>, '.'>]?: Condition<
72-
PropertyType<WithId<TSchema>, Property>
71+
[Property in Join<NestedPaths<WithId<TSchema>, true>, '.'>]?: Condition<
72+
PropertyType<WithId<TSchema>, Property, true>
7373
>;
7474
} & RootFilterOperators<WithId<TSchema>>);
7575

@@ -261,19 +261,9 @@ export type OnlyFieldsOfType<TSchema, FieldType = any, AssignableType = FieldTyp
261261
>;
262262

263263
/** @public */
264-
export type MatchKeysAndValues<TSchema> = Readonly<
265-
{
266-
[Property in Join<NestedPaths<TSchema>, '.'>]?: PropertyType<TSchema, Property>;
267-
} & {
268-
[Property in `${NestedPathsOfType<TSchema, any[]>}.$${`[${string}]` | ''}`]?: ArrayElement<
269-
PropertyType<TSchema, Property extends `${infer Key}.$${string}` ? Key : never>
270-
>;
271-
} & {
272-
[Property in `${NestedPathsOfType<TSchema, Record<string, any>[]>}.$${
273-
| `[${string}]`
274-
| ''}.${string}`]?: any; // Could be further narrowed
275-
}
276-
>;
264+
export type MatchKeysAndValues<TSchema> = Readonly<{
265+
[Property in Join<NestedPaths<TSchema, false>, '.'>]?: PropertyType<TSchema, Property, false>;
266+
}>;
277267

278268
/** @public */
279269
export type AddToSetOperators<Type> = {
@@ -474,75 +464,83 @@ export type Join<T extends unknown[], D extends string> = T extends []
474464
: string;
475465

476466
/** @public */
477-
export type PropertyType<Type, Property extends string> = string extends Property
478-
? unknown
479-
: Property extends keyof Type
480-
? Type[Property]
481-
: Property extends `${number}`
482-
? Type extends ReadonlyArray<infer ArrayType>
483-
? ArrayType
484-
: unknown
485-
: Property extends `${infer Key}.${infer Rest}`
486-
? Key extends `${number}`
487-
? Type extends ReadonlyArray<infer ArrayType>
488-
? PropertyType<ArrayType, Rest>
489-
: unknown
490-
: Key extends keyof Type
491-
? Type[Key] extends Map<string, infer MapType>
467+
export type PropertyType<
468+
Type,
469+
Property extends string,
470+
AllowToSkipArrayIndex extends boolean
471+
> = Type extends unknown
472+
? string extends Property
473+
? Type extends Map<string, infer MapType>
492474
? MapType
493-
: PropertyType<Type[Key], Rest>
494-
: unknown
495-
: unknown;
475+
: never
476+
:
477+
| (AllowToSkipArrayIndex extends false
478+
? never
479+
: Type extends ReadonlyArray<infer ArrayType>
480+
? PropertyType<ArrayType, Property, AllowToSkipArrayIndex>
481+
: never)
482+
| (Property extends keyof Type
483+
? Type[Property]
484+
: Property extends `${number | `$${'' | `[${string}]`}`}`
485+
? Type extends ReadonlyArray<infer ArrayType>
486+
? ArrayType
487+
: never
488+
: Property extends `${infer Key}.${infer Rest}`
489+
? Key extends `${number | `$${'' | `[${string}]`}`}`
490+
? Type extends ReadonlyArray<infer ArrayType>
491+
? PropertyType<ArrayType, Rest, AllowToSkipArrayIndex>
492+
: never
493+
: Key extends keyof Type
494+
? PropertyType<Type[Key], Rest, AllowToSkipArrayIndex>
495+
: never
496+
: never)
497+
: never;
496498

497499
/**
498500
* @public
499501
* returns tuple of strings (keys to be joined on '.') that represent every path into a schema
500502
* https://docs.mongodb.com/manual/tutorial/query-embedded-documents/
501503
*/
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 }
512-
? []
513-
: Type extends ReadonlyArray<infer ArrayType>
514-
? [] | [number, ...NestedPaths<ArrayType>]
515-
: Type extends Map<string, any>
516-
? [string]
517-
: Type extends object
518-
? {
519-
[Key in Extract<keyof Type, string>]: Type[Key] extends Type // type of value extends the parent
520-
? [Key]
521-
: // for a recursive union type, the child will never extend the parent type.
522-
// but the parent will still extend the child
523-
Type extends Type[Key]
524-
? [Key]
525-
: Type[Key] extends ReadonlyArray<infer ArrayType> // handling recursive types with arrays
526-
? Type extends ArrayType // is the type of the parent the same as the type of the array?
527-
? [Key] // yes, it's a recursive array type
528-
: // for unions, the child type extends the parent
529-
ArrayType extends Type
530-
? [Key] // we have a recursive array union
531-
: // child is an array, but it's not a recursive array
532-
[Key, ...NestedPaths<Type[Key]>]
533-
: // child is not structured the same as the parent
534-
[Key, ...NestedPaths<Type[Key]>] | [Key];
535-
}[Extract<keyof Type, string>]
536-
: [];
537-
538-
/**
539-
* @public
540-
* returns keys (strings) for every path into a schema with a value of type
541-
* https://docs.mongodb.com/manual/tutorial/query-embedded-documents/
542-
*/
543-
export type NestedPathsOfType<TSchema, Type> = KeysOfAType<
544-
{
545-
[Property in Join<NestedPaths<TSchema>, '.'>]: PropertyType<TSchema, Property>;
546-
},
547-
Type
548-
>;
504+
export type NestedPaths<Type, AllowToSkipArrayIndex extends boolean> = Type extends unknown
505+
? Type extends
506+
| string
507+
| number
508+
| boolean
509+
| Date
510+
| RegExp
511+
| Buffer
512+
| Uint8Array
513+
| ((...args: any[]) => any)
514+
| { _bsontype: string }
515+
? never
516+
: Type extends ReadonlyArray<infer ArrayType>
517+
? [
518+
...(
519+
| (AllowToSkipArrayIndex extends true ? [] : never)
520+
| [number | `$${'' | `[${string}]`}`]
521+
),
522+
...([] | NestedPaths<ArrayType, AllowToSkipArrayIndex>)
523+
]
524+
: Type extends Map<string, any>
525+
? [string]
526+
: Type extends object
527+
? {
528+
[Key in Extract<keyof Type, string>]: Type[Key] extends Type // type of value extends the parent
529+
? [Key]
530+
: // for a recursive union type, the child will never extend the parent type.
531+
// but the parent will still extend the child
532+
Type extends Type[Key]
533+
? [Key]
534+
: Type[Key] extends ReadonlyArray<infer ArrayType> // handling recursive types with arrays
535+
? Type extends ArrayType // is the type of the parent the same as the type of the array?
536+
? [Key] // yes, it's a recursive array type
537+
: // for unions, the child type extends the parent
538+
ArrayType extends Type
539+
? [Key] // we have a recursive array union
540+
: // child is an array, but it's not a recursive array
541+
[Key, ...([] | NestedPaths<Type[Key], AllowToSkipArrayIndex>)]
542+
: // child is not structured the same as the parent
543+
[Key, ...([] | NestedPaths<Type[Key], AllowToSkipArrayIndex>)];
544+
}[Extract<keyof Type, string>]
545+
: never
546+
: never;

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

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import {
1212
} from 'bson';
1313
import { expectAssignable, expectError, expectNotType, expectType } from 'tsd';
1414

15-
import { Collection, Filter, MongoClient, WithId } from '../../../../src';
15+
import { Collection, Filter, MongoClient, UpdateFilter, WithId } from '../../../../src';
1616

1717
/**
1818
* test the Filter type using collection.find<T>() method
@@ -406,3 +406,36 @@ nonSpecifiedCollection.find({
406406
hello: 'world'
407407
}
408408
});
409+
410+
// NODE-4513: improves support for union types and array operators
411+
type MyArraySchema = {
412+
nested: { array: { a: number; b: boolean }[] };
413+
something: { a: number } | { b: boolean };
414+
};
415+
416+
// "element" now refers to the name used in arrayFilters, it can be any string
417+
expectAssignable<UpdateFilter<MyArraySchema>>({
418+
$set: { 'nested.array.$[element]': { a: 2, b: false } }
419+
});
420+
expectAssignable<Filter<MyArraySchema>>({
421+
$set: { 'nested.array.$[element]': { a: 2, b: false } }
422+
});
423+
424+
// Specifying an identifier in the brackets is optional
425+
expectAssignable<UpdateFilter<MyArraySchema>>({
426+
$set: { 'nested.array.$[].a': 2 }
427+
});
428+
expectAssignable<Filter<MyArraySchema>>({
429+
$set: { 'nested.array.$[].a': 2 }
430+
});
431+
432+
// Union usage examples
433+
expectAssignable<Filter<MyArraySchema>>({
434+
'something.a': 2
435+
});
436+
expectError<Filter<MyArraySchema>>({
437+
'something.a': false
438+
});
439+
expectAssignable<Filter<MyArraySchema>>({
440+
'something.b': false
441+
});

0 commit comments

Comments
 (0)