Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature: Infer schema type automatically. #11563

Merged
merged 42 commits into from Jun 5, 2022

Conversation

mohammad0-0ahmad
Copy link
Contributor

@mohammad0-0ahmad mohammad0-0ahmad commented Mar 24, 2022

Tasks to be implemented :

  • Infer schema type automatically from document definition.
  • Provide TS utility to be able to extract schema type from schema instance to be able to reuse the type as separated type "InferSchemaType".
  • Improve Model.create function type to show schema properties in suggestions dialog.
  • Adding "statics" property in schema options to support auto type inference for model statics methods.
  • Adding "methods" property in schema options to support auto type inference for document instance methods.
  • Support custom type key "typeKey" to inference schema type.
  • Improve Model new function "document constructor" type to show schema properties in suggestions dialog.
  • Improve Model.insertMany function type to show schema properties in suggestions dialog.
  • Adding "query" property in schema options to support auto type inference for query helpers methods.

Status:

  • Ready to review.
  • Docs is updated.
  • Ready to merge.

Do you have any suggestions or ideas to be implemented in this PR, please leave a comment 🙂

Get it early:

You can install mongoose including this feature by running:

 npm i Automattic/mongoose#pull/11563/head

Screenshots :

  • Auto schema type inference & create function:

Untitled

  • InferSchemaType utility & supporting custom type key:

Untitled

@Uzlopak
Copy link
Collaborator

Uzlopak commented Apr 5, 2022

@mohammad0-0ahmad

When do you think this PR will be ready for merge?

@mohammad0-0ahmad
Copy link
Contributor Author

mohammad0-0ahmad commented Apr 5, 2022

@mohammad0-0ahmad

When do you think this PR will be ready for merge?

When it is reviewed 🙂 @Uzlopak

Copy link
Collaborator

@Uzlopak Uzlopak left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not easy to digest :D

Made some comments.

test/document.test.js Outdated Show resolved Hide resolved
test/model.test.js Outdated Show resolved Hide resolved
test/query.test.js Outdated Show resolved Hide resolved
test/query.test.js Outdated Show resolved Hide resolved
test/types/document.test.ts Outdated Show resolved Hide resolved
test/types/models.test.ts Outdated Show resolved Hide resolved
@mohammad0-0ahmad
Copy link
Contributor Author

mohammad0-0ahmad commented Apr 5, 2022

@Uzlopak
Thanks for reviewing and following the entire process.
I've resolved the latest comments, and I am looking forward for @vkarpov15 review.
Best regards! 🙂

@Uzlopak
Copy link
Collaborator

Uzlopak commented Apr 8, 2022

@vkarpov15
I think this should be part of v6.3

@Uzlopak Uzlopak linked an issue Apr 8, 2022 that may be closed by this pull request
@Uzlopak
Copy link
Collaborator

Uzlopak commented Apr 8, 2022

Maybe we should talk about documenting https://github.com/Automattic/mongoose/blob/master/docs/typescript.md

@mohammad0-0ahmad
Copy link
Contributor Author

Hello @vkarpov15!
Would you like to review this?

@uncle-ara
Copy link

when will typing become available for general use?

@mohammad0-0ahmad
Copy link
Contributor Author

when will typing become available for general use?

I am waiting for @vkarpov15 review.

@Uzlopak
Copy link
Collaborator

Uzlopak commented Apr 22, 2022

@mohammad0-0ahmad

I hope you are still motivated. Issue is that complex typescript PR are always leaving an unease feeling in the stomach. So dont take it personally. :)

Can you check please if we need some documentation changes?

https://github.com/Automattic/mongoose/tree/master/docs/typescript

@mohammad0-0ahmad
Copy link
Contributor Author

mohammad0-0ahmad commented Apr 22, 2022

@mohammad0-0ahmad

I hope you are still motivated. Issue is that complex typescript PR are always leaving an unease feeling in the stomach. So dont take it personally. :)

Can you check please if we need some documentation changes?

https://github.com/Automattic/mongoose/tree/master/docs/typescript

I am still motivated, and I understand that, but I've provided some JS docs for the types, and I ready to explain the unclear things in this PR.
We need absolutely to make some documentation changes and maybe update docs on the website, but I would like to wait until get review response in case something will be not acceptable.

Copy link
Collaborator

@vkarpov15 vkarpov15 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I love the type key support and auto typed schemas. But my big concerns are 1) performance, 2) backwards btreaking changes. Our TS benchmark shows before this PR:

Run node ./benchmarks/typescript
simple instantiations: 35805
simple memory used: 152972.7
simple check time: 3.089
simple total time: 4.831999999999999

After this PR:

Run node ./benchmarks/typescript
simple instantiations: 227139
simple memory used: 191137.1
simple check time: 3.8959999999999995
simple total time: 5.3

After spending over 6 months fixing the performance of our TypeScript types, I do not want to merge something that undoes all the progress we made in #10349.

types/document.d.ts Show resolved Hide resolved
types/index.d.ts Outdated Show resolved Hide resolved
types/index.d.ts Outdated Show resolved Hide resolved
types/schemaoptions.d.ts Show resolved Hide resolved
types/index.d.ts Show resolved Hide resolved
Copy link
Collaborator

@vkarpov15 vkarpov15 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice, thanks 👍 We'll merge this into the 6.4 branch. Thanks for all your hard work on this.

@vkarpov15 vkarpov15 added this to the 6.4 milestone Jun 5, 2022
@vkarpov15 vkarpov15 changed the base branch from master to 6.4 June 5, 2022 17:53
@vkarpov15 vkarpov15 merged commit f48eee2 into Automattic:6.4 Jun 5, 2022
@mohammad0-0ahmad
Copy link
Contributor Author

Thank you @Uzlopak @vkarpov15 as well.

@magyarb
Copy link

magyarb commented Jun 14, 2022

Hi, and thanks for all the work done here.

I see that mongoose-tsgen is no longer updated due to this PR.

Will it be possible to export these inferred types as separate files? It would be useful to use these types or interfaces elsewhere, for example on the frontend, where mongoose is not installed, or converting them to json schemas, to perform input validation on both the frontend and backend.

@francescov1
Copy link
Contributor

@magyarb I'm open to continuing support for mongoose-tsgen if it is still useful to the community. I assumed that it became redundant with the type inference, but the use case for exporting types still exists so might be worth maintaining for now.

We could also look at integrating something like it into Mongoose natively if that's something the team is interested in. Happy to help in whatever method is preferred!

@mohammad0-0ahmad
Copy link
Contributor Author

mohammad0-0ahmad commented Jun 15, 2022

It might be great if you can make it availabe internally @francescov1 or at least mention that mongoose-tsgen can generate types files in mongoose docs.
What do you think @vkarpov15 @Uzlopak @AbdelrahmanHafez ?

@vkarpov15
Copy link
Collaborator

@francescov1 I'd say continue to support mongoose-tsgen for now 👍

@francescov1
Copy link
Contributor

@vkarpov15 Will do 👍

@@ -101,7 +101,15 @@ declare module 'mongoose' {
[k: string]: any
}

export type Require_id<T> = T extends { _id?: any } ? (T & { _id: T['_id'] }) : (T & { _id: Types.ObjectId });
export type Require_id<T> = T extends { _id?: infer U }
? U extends any
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry that I'm not excellent in Typescript, but what does U extends any here mean?

As far as I know, everything extends any? so the contiditional clause (T & { _id: Types.ObjectId }) is always used, and the clause T & Required<{ _id: U }> is never used.

Thus, _id will be always infered with type ObjectId, even if I have defined another type such as String in the Schema definition.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For example:

import {model, Schema} from "mongoose";
const schema_with_string_id = new Schema({_id: String, nickname: String})
const theModel = model('test', schema_with_string_id)
const obj = new theModel()
const theId = obj._id

The type of variable theId will be string | Types.ObjectId, rather than string.
Is this designed behaviour for some reason I don't know, or just a bug?

Copy link

@Starrah Starrah Jul 7, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mohammad0-0ahmad @Uzlopak Sorry for bothering you!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hello @Starrah !
Firstly, you are not bothering, and you are totally right.
I think this meant to be something like:

  export type Require_id<T> = T extends { _id?: infer U }
    ? IfEquals<U, any> extends true
      ? (T & { _id: Types.ObjectId })
      : T & Required<{ _id: U }>
    : T & { _id: Types.ObjectId };

Thanks for your review, I will make sure to refactor this.
What are your thoughts @Uzlopak about that?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I did this to check if T is not never.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I did this to check if T is not never.

First, does you mean you did that to check if {_id: } in T is not never, rather than check T?
Because according to my experiment,

import {Require_id, Types} from "mongoose";

type Required_id_old<T> = T extends { _id?: any } ? (T & { _id: T['_id'] }) : (T & { _id: Types.ObjectId });
type Required_id_new<T> = T extends { _id?: infer U }
    ? U extends any
        ? (T & { _id: Types.ObjectId })
        : T & Required<{ _id: U }>
    : T & { _id: Types.ObjectId };

type t1_old = Required_id_old<{_id: never}>
type t1_new = Required_id_new<{_id: never}>

type t2_old = Required_id_old<never>
type t2_new = Required_id_new<never>

Both t2_old and t2_new is never(which I think is expected behaviour), and for t1, t1_old is {_id: never} but t1_new is never(I have no idea which is your expected behaviour).

Then, if my assumption is true and you does mean check if {_id: } in T is not never, then maybe the two conditional clause should be swapped, as the following:

type Required_id_new<T> = T extends { _id?: infer U }
    ? U extends any
        ? T & Required<{ _id: U }>
        : (T & { _id: Types.ObjectId })
    : T & { _id: Types.ObjectId };

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

By the way, my Typescript version is 4.7.2. I surprisingly found that T extends any? true: false equals to never when T is never, because the following code:

type Test<T> = T extends any ? true : false
type A = Test<never>
const a: A = false

gives error output

Error:(3, 7) TS2322: Type 'boolean' is not assignable to type 'never'.

The behaviour is the same in Typescript 4.6.3.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well, I guess, I should have implemented appropriate typings tests.

Copy link

@Starrah Starrah Jul 8, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mohammad0-0ahmad @Uzlopak
Though with the discussion above, we (or just I) cannot still figure out what the codes here means, specifically, what does the U extends any mean.

However, I want to point out that the following test case written by me, got failed:

// index.test-d.ts
import {model, Schema} from "mongoose";
import {expectType} from "tsd";

const schema_with_string_id = new Schema({_id: String, nickname: String})
const theModel = model('test', schema_with_string_id)
const obj = new theModel()

expectType<string>(obj._id)

when testing it with tsd, gives the following error:

 index.test-d.ts:8:0
  ✖  8:0  Parameter type string is declared too wide for argument type string & ObjectId.

According to @mohammad0-0ahmad :

Hello @Starrah ! Firstly, you are not bothering, and you are totally right. I think this meant to be something like:

  export type Require_id<T> = T extends { _id?: infer U }
    ? IfEquals<U, any> extends true
      ? (T & { _id: Types.ObjectId })
      : T & Required<{ _id: U }>
    : T & { _id: Types.ObjectId };

Thanks for your review, I will make sure to refactor this. What are your thoughts @Uzlopak about that?

I think that the test case failure above indicates a bug. So, I decide to raise a new issue (#12070) to report and/or further discuss this bug.

@olawalejuwonm
Copy link

Does this work for method too?

@Automattic Automattic locked and limited conversation to collaborators May 1, 2023
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Typescript type inference from mongoose Schema
8 participants