Skip to content

Strict Projection Object Typing #13840

Closed
Closed
@pshaddel

Description

@pshaddel

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);

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementThis issue is a user-facing general improvement that doesn't fix a bug or add a new featurenew featureThis change adds new functionality, like a new method or class

    Type

    No type

    Projects

    No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions