Automattic / mongoose

MongoDB object modeling designed to work in an asynchronous environment.
https://mongoosejs.com
MIT License
26.91k stars 3.84k forks source link

[Types] Query with dotted paths #12064

Open hasezoey opened 2 years ago

hasezoey commented 2 years ago

Prerequisites

Issue

Currently Query's do not have dotted path suggestions, but would be possible with current typescript.

See This Stackoverflow Answer

TL;DR: a simple example of what the stackoverflow answer has does (note: the types are directly copied from the answer)

interface Test {
  somenested?: {
    path: string;
  };
  somethingelse?: string;
}

type PathsToStringProps<T> = T extends string
  ? []
  : {
      [K in Extract<keyof T, string>]: [K, ...PathsToStringProps<T[K]>];
    }[Extract<keyof T, string>];

type Join<T extends string[], D extends string> = T extends []
  ? never
  : T extends [infer F]
  ? F
  : T extends [infer F, ...infer R]
  ? F extends string
    ? `${F}${D}${Join<Extract<R, string[]>, D>}`
    : never
  : string;

function onlyDotted(input: Join<PathsToStringProps<Test>, '.'>) {
  console.log('input', input);
}

onlyDotted('somenested.path'); // works, with suggestions
onlyDotted('somethingelse'); // also works, with suggestions
onlyDotted('ERROR'); // error, property does not exist

Disclaimer: i am by no means a expert at complex typescript types, i also have no clue about the effect on types performance this would bring

hasezoey commented 2 years ago

just noticed that #10180 is a similar Issue, should that one be closed in favor of this (or vise versa)?

vkarpov15 commented 2 years ago

I'm really wary of doing this because I imagine it'll be horrifically slow and cause a lot of impossible to debug "Type instantiation is excessively deep and possibly infinite" type errors.

I've tried adding the following to query.d.ts:

  type PathsToStringProps<T> = T extends string
    ? []
    : {
        [K in Extract<keyof T, string>]: [K, ...PathsToStringProps<T[K]>];
      }[Extract<keyof T, string>];

  type Join<T extends string[], D extends string> = T extends []
    ? never
    : T extends [infer F]
    ? F
    : T extends [infer F, ...infer R]
    ? F extends string
      ? `${F}${D}${Join<Extract<R, string[]>, D>}`
      : never
    : string;

  type _FilterQuery<T> = {
    [P in keyof T]?: Condition<T[P]>;
  } & RootQuerySelector<T>;

  /**
   * Filter query to select the documents that match the query
   * @example
   * ```js
   * { age: { $gte: 30 } }
   * ```
   */
  type FilterQuery<T> = _FilterQuery<T> & Join<PathsToStringProps<T>, '.'>;

And I'm getting the following:

../mongoose/types/query.d.ts:34:48 - error TS2321: Excessive stack depth comparing types 'PathsToStringProps<T>' and 'string[]'.

34   type FilterQuery<T> = _FilterQuery<T> & Join<PathsToStringProps<T>, '.'>;
                                                  ~~~~~~~~~~~~~~~~~~~~~

Found 1 error in ../mongoose/types/query.d.ts:34

Haven't been able to find a workaround. Any suggestions @hasezoey ?

hasezoey commented 2 years ago

Haven't been able to find a workaround. Any suggestions @hasezoey ?

no i dont have any solutions, and yes it would cause may "too deep" errors, because that is basically what this approach is "meant" to do, the only ways i could see are:

i had just noted it here because it was now possible and mongoose already uses it in places at runtime but didnt have it in types

PCOffline commented 1 year ago

I stumbled upon this myself a couple of days ago and came up with a solution (whose performance is questionable but perhaps a bit better): https://tsplay.dev/wO2xRW

PCOffline commented 1 year ago

Hey, I came up with a solution, as seen above ^ This solution is very performant – according to my tests, the compilation and emit of the type file and a file that uses this type take around 20ms. The implementation makes FilterQuery more strict by overriding the ApplyBasicQueryCasting to avoid the unnecessary any in the union. The two main types are DeepNestedAccess and RecursiveFieldsOfObject. DeepNestedAccess is a way to access elements in objects and arrays via dot-notation strings (e.g. 'array.0.field' will actually access the 'field' element in the first element of the array). RecursiveFieldsOfObject gets all the paths and nested paths of an object, including arrays.

PCOffline commented 1 year ago

I'll provide some screenshots and sources for my tests and open a PR in a sec

vkarpov15 commented 4 months ago

Unfortunately @PCOffline 's solution doesn't work too well, I tried it for #14615 but unfortunately results in an infinite type instantiation error in automatic schema inference.

../mongoose/types/index.d.ts:90:3 - error TS2589: Type instantiation is excessively deep and possibly infinite.

 90   HydratedDocument<
      ~~~~~~~~~~~~~~~~~
 91   InferSchemaType<TSchema>,
    ~~~~~~~~~~~~~~~~~~~~~~~~~~~
... 
 93   ObtainSchemaGeneric<TSchema, 'TQueryHelpers'>
    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 94   >,
    ~~~

Realistically, I don't think we can support this unless TypeScript has some sort of way to limit the depth of the recursion or avoiding infinite recursion.