Closed
Description
Prerequisites
- I have written a descriptive issue title
- I have searched existing issues to ensure the feature has not already been requested
🚀 Feature Proposal
A Projection Object with strict typescript type that:
- Accepts only allowed values of MongoDB Projection object
- Supports Typing of Nested Objects in Projection
- Gives TS Errors on Inclusion and Exclusion
Motivation
1- Passing MongoDB expected values for projection
Based on the documentation only 1
, 0
, false
, or true
should be passed(it works also with other numbers).
Example:
const Cat = mongoose.model('Cat', new Schema({ name: String }));
await Cat.find({}, { name: "true" }) // No TS Error - Returned Value is "true" as string
2- No Typing for Nested Objects in Projection
const Cat = mongoose.model('Cat', new Schema({ name: String, childs: [{ name: String }] }));
await Cat.find({}, { childs: { ... } }) // Inside the `childs` we do not have typing.
3- Inclusion / Exclusion Rules
In projection if it is a inclusion projection, exclusion is not allowed and vice versa. But this could happen and it results in run time error:
const Cat = mongoose.model('Cat', new Schema({ name: String, family: String }));
await Cat.findOne({}, { name: true, family: false }) // Runtime Error after running the query
Example
I have already implemented a type that I think covers most cases and I would be happy to make the regarding PR if this change is acceptable:
type Projector<T, Element> = T extends Array<infer U> ? Projector<U, Element> : T extends object ? {
[K in keyof T]?: T[K] extends object ? Projector<T[K], Element> | Element : Element;
} : Element;
type InclusionProjection<T> = { _id?: true | 1 | false | 0 } & Omit<Projector<T, true | 1>, '_id'>;
type ExclusionProjection<T> = { _id?: false | 0 } & Omit<Projector<T, false | 0>, '_id'>;
export type ProjectionType<T> = InclusionProjection<T> | ExclusionProjection<T>; // New Projection Type
type RawDocType = {
_id: string;
a: string;
b: {
c: {
d: string;
}
},
g: string[];
f: {
_id: string;
starts: number
}[]
}
type Project = ProjectionType<RawDocType>;
const obj0: Project = { _id: false, a: false, b: { c: { d: false } } };// Allowed
const obj1: Project = { _id: 1, a: 1 };// Allowed: 0 and 1 are allowed
const obj2: Project = { _id: false, a: false, b: { c: { d: false } }, name: false };// Not allowed name is not a key of RawDocType
const obj3: Project = { _id: true, a: true, b: { c: { d: false } } };// Not allowed you can't exclude in a inclusion object
const obj4: Project = { _id: false, a: false, b: { c: { d: true } } };// Not allowed you can't include in a exclusion object
const obj5: Project = { a: true, _id: false };// Allowed _id is an exception in inclusion object
const obj6: Project = { b: { c: true } };// Allowed: Nested Object Typing is Supported
const obj7: Project = { b: true };// Allowed: You can project the whole object instead of its keys
const obj8: Project = { a: 'str' }; // Not allowed you can't assign a string to a key in projection object
const obj9: Project = { a: 3 }; // Not allowed you can't assign a number to a key in projection object
const obj10: Project = { g: 1, f: { starts: 1 } }; // Allowed Should correctly support array of objects and array of strings
It is testable here in the TS Playground.
Something like this in the source code could solve the issue:
type Projector<T, Element> = T extends Array<infer U> ? Projector<U, Element> : T extends object ? {
[K in keyof T]?: T[K] extends object ? Projector<T[K], Element> | Element : Element;
} : Element;
type _IDType = { _id?: boolean | 1 | 0 }
type InclusionProjection<T> = NestedPartial<Projector<NestedRequired<T>, true | 1> & _IDType>;
type ExclusionProjection<T> = NestedPartial<Projector<NestedRequired<T>, false | 0> & _IDType>;
type NestedRequired<T> = T extends Array<infer U>
? Array<NestedRequired<U>>
: T extends object
? {
[K in keyof T]-?: NestedRequired<T[K]>;
}
: T;
type NestedPartial<T> = T extends object
? {
[K in keyof T]?: NestedPartial<T[K]>;
}
: T;
export type ProjectionType<T> = (InclusionProjection<T> | ExclusionProjection<T>) & AnyObject | string | ((...agrs: any) => any);