Skip to content

fix: Strict Projection Object Typing #15327

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

Open
wants to merge 18 commits into
base: master
Choose a base branch
from

Conversation

pshaddel
Copy link
Contributor

@pshaddel pshaddel commented Mar 22, 2025

Summary
Fixes this issue: #13840

This was a PR which was merged and then reverted for releasing of 8.1

Checkout the PR, as this is already once reviewed and discussed: #13993

I just rebased the branch and pushed a commit to fix the TS tests.

@pshaddel
Copy link
Contributor Author

pshaddel commented Apr 3, 2025

@vkarpov15 Hey, can you take a look at this? :)

@vkarpov15 vkarpov15 added this to the 8.14 milestone Apr 23, 2025
@vkarpov15 vkarpov15 requested review from hasezoey and Copilot April 23, 2025 15:48
Copy link

@Copilot Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull Request Overview

This PR fixes strict projection object typing issues as described in the related GitHub issue. Key changes include updating the chaining type test for schema plugins and enhancing TS tests for projection validations in queries.

Reviewed Changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated no comments.

File Description
test/types/schema.test.ts Updated plugin chaining test to validate strict type preservation.
test/types/queries.test.ts Added and updated TS assertions for projection types and query validations.

Test.find({}, { child: 1 }); // Dot notation should be able to use a combination with objects
Test.find({}, { 'docs.profiles': { name: 1 } }); // should support a combination of dot notation and objects
expectError(Test.find({}, { 'docs.profiles': { name: 'aa' } })); // should support a combination of dot notation and objects
expectError(Test.find({}, { endDate: { toString: 1 } }));
Copy link
Collaborator

Choose a reason for hiding this comment

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

I'd love to see a test of using as ProjectionType<> to explicitly cast in case the type checking is incorrect for some reason (or if user is dealing with 5-level deep path)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@vkarpov15
something like this you mean?

Test.find({}, { docs: { unknownParams: 1 }} as ProjectionType<ITest>);

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@vkarpov15 vkarpov15 modified the milestones: 8.14, 8.15 Apr 25, 2025
Copy link
Collaborator

@hasezoey hasezoey left a comment

Choose a reason for hiding this comment

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

Looks OK to me, but makes typegoose's types not work anymore. Some of the errors i could workaround, but the last one i couldnt find the cause of, also bisecting this PR only caused errors everywhere.

As a side question, why change some unknown's to anys?

Typegoose Errors in more detail

Errors without any modifications:

$ tsc -p tsconfig.buildTests.json
src/typegoose.ts:103:9 - error TS2322: Type 'Model<ObtainDocumentType<any, DocumentType<InstanceType<U & Func>>, ResolveSchemaOptions<DefaultSchemaOptions>>, ... 4 more ..., Schema<...>>' is not assignable to type 'Model<any, {}, {}, {}, any, any>'.
  The types returned by 'findById(...).exec()' are incompatible between these types.
    Type 'Promise<(ObtainDocumentType<any, DocumentType<InstanceType<U & Func>>, ResolveSchemaOptions<DefaultSchemaOptions>> extends any[] ? Default__v<...>[] : Default__v<...>) | null>' is not assignable to type 'Promise<(FlattenMaps<any> & Required<{ _id: unknown; }> & { __v: number; })[] | (FlattenMaps<any> & Required<{ _id: unknown; }> & { __v: number; }) | null>'.
      Type '(ObtainDocumentType<any, DocumentType<InstanceType<U & Func>>, ResolveSchemaOptions<DefaultSchemaOptions>> extends any[] ? Default__v<...>[] : Default__v<...>) | null' is not assignable to type '(FlattenMaps<any> & Required<{ _id: unknown; }> & { __v: number; })[] | (FlattenMaps<any> & Required<{ _id: unknown; }> & { __v: number; }) | null'.

103   const compiledModel: mongoose.Model<any> = modelFn(name, buildSchema(cl, mergedOptions));
            ~~~~~~~~~~~~~

src/typeguards.ts:21:12 - error TS2677: A type predicate's type must be assignable to its parameter's type.
  Type 'Array<DocumentType<NonNullable<T>>>' is not assignable to type 'Array<Ref<T, S>>'.
    Type 'DocumentType<NonNullable<T>>' is not assignable to type 'Ref<T, S>'.
      Type 'DocumentType<NonNullable<T>>' is not assignable to type 'DocumentType<T, BeAnObject>'.
        Type 'DocumentType<NonNullable<T>>' is not assignable to type 'Document<unknown, BeAnObject, T>'.

21 ): docs is mongoose.Types.Array<DocumentType<NonNullable<T>>>;
              ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

src/typeguards.ts:27:102 - error TS2677: A type predicate's type must be assignable to its parameter's type.
  Type 'DocumentType<NonNullable<T>>[]' is not assignable to type 'Ref<T, S>[]'.
    Type 'DocumentType<NonNullable<T>>' is not assignable to type 'Ref<T, S>'.
      Type 'DocumentType<NonNullable<T>>' is not assignable to type 'DocumentType<T, BeAnObject>'.
        Type 'DocumentType<NonNullable<T>>' is not assignable to type 'Document<unknown, BeAnObject, T>'.

27 export function isDocumentArray<T, S extends RefType>(docs: Ref<T, S>[] | null | undefined): docs is DocumentType<NonNullable<T>>[];
                                                                                                        ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~


Found 3 errors in 2 files.

Errors  Files
     1  src/typegoose.ts:103
     2  src/typeguards.ts:21
error Command failed with exit code 2.

Errors with modifictions:

$ tsc -p tsconfig.buildTests.json
src/typegoose.ts:103:9 - error TS2322: Type 'Model<ObtainDocumentType<any, DocumentType<InstanceType<U & Func>>, ResolveSchemaOptions<DefaultSchemaOptions>>, ... 4 more ..., Schema<...>>' is not assignable to type 'Model<any, {}, {}, {}, any, any>'.
  The types returned by 'findById(...).exec()' are incompatible between these types.
    Type 'Promise<(ObtainDocumentType<any, DocumentType<InstanceType<U & Func>>, ResolveSchemaOptions<DefaultSchemaOptions>> extends any[] ? Default__v<...>[] : Default__v<...>) | null>' is not assignable to type 'Promise<(FlattenMaps<any> & Required<{ _id: unknown; }> & { __v: number; })[] | (FlattenMaps<any> & Required<{ _id: unknown; }> & { __v: number; }) | null>'.
      Type '(ObtainDocumentType<any, DocumentType<InstanceType<U & Func>>, ResolveSchemaOptions<DefaultSchemaOptions>> extends any[] ? Default__v<...>[] : Default__v<...>) | null' is not assignable to type '(FlattenMaps<any> & Required<{ _id: unknown; }> & { __v: number; })[] | (FlattenMaps<any> & Required<{ _id: unknown; }> & { __v: number; }) | null'.

103   const compiledModel: mongoose.Model<any> = modelFn(name, buildSchema(cl, mergedOptions));
            ~~~~~~~~~~~~~


Found 1 error in src/typegoose.ts:103

error Command failed with exit code 2.

Modifications:

diff --git a/src/typeguards.ts b/src/typeguards.ts
index 52886460..7e62e66c 100644
--- a/src/typeguards.ts
+++ b/src/typeguards.ts
@@ -18,13 +18,13 @@ export function isDocument<T, S extends RefType>(doc: Ref<T, S> | null | undefin
  */
 export function isDocumentArray<T, S extends RefType>(
   docs: mongoose.Types.Array<Ref<T, S>> | null | undefined
-): docs is mongoose.Types.Array<DocumentType<NonNullable<T>>>;
+): docs is mongoose.Types.Array<DocumentType<T>>;
 /**
  * Check if the given array is fully populated
  * Only returns "true" if all members in the array are populated
  * @param docs The Array of Refs with uncertain type
  */
-export function isDocumentArray<T, S extends RefType>(docs: Ref<T, S>[] | null | undefined): docs is DocumentType<NonNullable<T>>[];
+export function isDocumentArray<T, S extends RefType>(docs: Ref<T, S>[] | null | undefined): docs is DocumentType<T>[];
 export function isDocumentArray(docs: Ref<any, any>[] | null | undefined): unknown {
   // its "any" & "unknown" because this is not listed as an overload
   return Array.isArray(docs) && docs.every((v) => isDocument(v));

Note that everything runs fine without this PR.

Also finally, with this PR has compile times of ~30s and also kinda high resource (cpu, ram) usage, though this might just be because its taking error paths.
Normal time is around ~7s for typegoose (before this PR) and way lower resource usage.

Typegoose at b3ad7940b96baf9b3a46573bb417479be8bf94d8.

I dont really see how just changing QueryOptions projection could cause those errors. (i already tried reverting some of the other changes regarding unknown -> any to rule them out)

As a final note, i think some of those types should maybe live in their own file to be better organized than in big type files, though that might not be in scope for this PR.

EDIT: sorry, i had some issues while posting this, so the original message was incomplete and i didnt know it was posted, hence my next (hidden) message

types/query.d.ts Outdated
@@ -128,7 +128,7 @@ declare module 'mongoose' {
updatedAt?: boolean;
}

interface QueryOptions<DocType = unknown> extends
interface QueryOptions<DocType = any> extends
Copy link
Collaborator

Choose a reason for hiding this comment

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

Isnt DocType now unused here again?

Copy link
Contributor Author

@pshaddel pshaddel Apr 25, 2025

Choose a reason for hiding this comment

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

That is true. QueryOptions is used in lots of places.

The projection is passed like this, I am not sure why the projection is used in this interface(or if anyone uses this, instead of passing it as the second - because you are already passing the projection as one argument). it causes some circular reference as well.
276666875-1b0a8a53-f3ba-4a05-ab53-ebe99f81a249

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@hasezoey Just added a new commit. leaving the projection in the QueryOptions as it was before(not using ProjectionType).

I am trying to find out why Typegoose is facing errors, the one that I get is this one:

Type 'Model<ObtainDocumentType<any, DocumentType<InstanceType<U & Func>>, ResolveSchemaOptions<DefaultSchemaOptions>>, ... 4 more ..., Schema<...>>' is not assignable to type 'Model<any, {}, {}, {}, any, any>'.
  The types returned by 'findById(...).exec()' are incompatible between these types.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Yes that is the error i could not figure out why it was happening.
I mean i know that typegoose is not 100% correct with mongoose types, but it worked before this PR (and on master) and this error just seems unrelated.

hasezoey

This comment was marked as outdated.

@pshaddel
Copy link
Contributor Author

@vkarpov15 After pulling master, the number of instantiation exceeds.
I expected some more instantiations, because of the dot support and handling of nested objects to depth 4.

TypeScript Diagnostics Comparison

Metric Master This Branch
Files 193 193
Lines of Library 38,658 38,658
Lines of Definitions 77,536 77,639
Lines of TypeScript 20 20
Lines of JavaScript 0 0
Lines of JSON 0 0
Lines of Other 0 0
Identifiers 103,996 104,211
Symbols 254,594 244,474
Types 84,529 85,807
Instantiations 266,799 279,715
Memory used 272,070K 269,809K
Assignability cache size 3,247 3,293
Identity cache size 171 184
Subtype cache size 2 2
Strict subtype cache size 4 4
I/O Read time 0.02s 0.03s
Parse time 0.46s 0.49s
ResolveModule time 0.05s 0.05s
ResolveTypeReference time 0.01s 0.01s
ResolveLibrary time 0.02s 0.01s
Program time 0.60s 0.63s
Bind time 0.24s 0.28s
Check time 2.68s 2.81s
transformTime time 0.01s 0.01s
commentTime time 0.00s 0.00s
I/O Write time 0.00s 0.00s
printTime time 0.01s 0.01s
Emit time 0.01s 0.02s
Total time 3.54s 3.73s

Poorshad added 2 commits April 25, 2025 23:17
For building the Projection, we need the interface, not the DocType
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants