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

If I explicitly set the type of a PopulatedDoc variable using populate in a query, the virtual variables of the retrieved instances become nonexistent #15111

Closed
2 tasks done
nikzanda opened this issue Dec 17, 2024 · 1 comment · Fixed by #15132
Labels
confirmed-bug We've confirmed this is a bug in Mongoose and will fix it. typescript Types or Types-test related issue / Pull Request
Milestone

Comments

@nikzanda
Copy link

Prerequisites

  • I have written a descriptive issue title
  • I have searched existing issues to ensure the bug has not already been reported

Mongoose version

8.9.1

Node.js version

20.10.0

MongoDB server version

6.0.2

Typescript version (if applicable)

5.5.4

Description

It seems there is a typing conflict when executing a query using populate and explicitly setting the type of the PopulatedDoc variable. It's likely that the virtual variables are being overwritten.

As you can see from the example, in the query I explicitly declare the type of the PopulatedDoc variable "child". After that, if I try to add one of the retrieved instances to an array of type ParentInstance, I get a typing error. The same virtual variables no longer exist, and the linter returns an error.

const filteredParents: ParentInstance[] = [];
const parents = await Parent.find().populate<{ child: ChildInstance }>(
  'child'
);
parents.forEach((parent) => {
  const condition = parent.fullName === 'fake condition'; // error
  if (condition) {
    filteredParents.push(parent); // error
  }
});

The full error:

Argument of type 'Document<unknown, {}, MergeType<IParent, { child: Document<unknown, {}, IChild> & Omit<IChild & Required<{ _id: ObjectId; }> & { __v: number; }, "id"> & IChildVirtuals; }>> & Omit<...> & { ...; } & Required<...> & { ...; }' is not assignable to parameter of type 'Document<unknown, {}, IParent> & Omit<IParent & Required<{ _id: ObjectId; }> & { __v: number; }, keyof IParentVirtuals> & IParentVirtuals'.
  Property 'fullName' is missing in type 'Document<unknown, {}, MergeType<IParent, { child: Document<unknown, {}, IChild> & Omit<IChild & Required<{ _id: ObjectId; }> & { __v: number; }, "id"> & IChildVirtuals; }>> & Omit<...> & { ...; } & Required<...> & { ...; }' but required in type 'IParentVirtuals'.(2345)
index.ts(64, 3): 'fullName' is declared here.

ParentInstance is declared like this:

type ParentInstance = HydratedDocument<
  IParent,
  ParentDocumentOverrides & IParentVirtuals
>;

If I remove & IParentVirtuals from the declaration, the error disappears.

Steps to Reproduce

Reproduction link here.

Expected Behavior

If I explicitly set the type of a PopulatedDoc variable, I should still be able to use the virtual variables of the retrieved instances.

@vkarpov15 vkarpov15 added this to the 8.9.2 milestone Dec 18, 2024
@vkarpov15 vkarpov15 added the typescript Types or Types-test related issue / Pull Request label Dec 18, 2024
@IslandRhythms IslandRhythms added the confirmed-bug We've confirmed this is a bug in Mongoose and will fix it. label Dec 18, 2024
@IslandRhythms
Copy link
Collaborator

import {
    connection,
    Connection,
    Document,
    HydratedDocument,
    Model,
    PopulatedDoc,
    Schema,
    SchemaTypes,
    Types,
  } from 'mongoose';
  
  interface IChild {
    _id: Types.ObjectId;
    name: string;
  }
  
  type ChildDocumentOverrides = {};
  
  interface IChildVirtuals {
    id: string;
  }
  
  type ChildInstance = HydratedDocument<
    IChild,
    ChildDocumentOverrides & IChildVirtuals
  >;
  
  type ChildModelType = Model<
    IChild,
    {},
    ChildDocumentOverrides,
    IChildVirtuals,
    ChildInstance
  >;
  
  const createChildModel = (connection: Connection) => {
    const childSchema = new Schema<IChild, ChildModelType>(
      {
        name: {
          type: SchemaTypes.String,
          required: true,
          trim: true,
        },
      },
      {
        optimisticConcurrency: true,
      }
    );
    return connection.model<IChild, ChildModelType>('Child', childSchema);
  };
  
  interface IParent {
    _id: Types.ObjectId;
    name: string;
    surname: string;
    child: PopulatedDoc<Document<Types.ObjectId> & IChild>;
  }
  
  type ParentDocumentOverrides = {};
  
  interface IParentVirtuals {
    id: string;
    fullName: string;
  }
  
  type ParentInstance = HydratedDocument<
    IParent,
    ParentDocumentOverrides & IParentVirtuals
  >;
  
  type ParentModelType = Model<
    IParent,
    {},
    ParentDocumentOverrides,
    IParentVirtuals,
    ParentInstance
  >;
  
  const createParentModel = (connection: Connection) => {
    const parentSchema = new Schema<IParent, ParentModelType>(
      {
        name: {
          type: SchemaTypes.String,
          required: true,
          trim: true,
        },
        surname: {
          type: SchemaTypes.String,
          required: true,
          trim: true,
        },
        child: {
          type: SchemaTypes.ObjectId,
          ref: 'Child',
          required: true,
        },
      },
      { optimisticConcurrency: true }
    );
  
    parentSchema.virtual('fullName').get(function () {
      return `${this.name} ${this.surname}`;
    });
  
    return connection.model<IParent, ParentModelType>('Parent', parentSchema);
  };
  
  const Child = createChildModel(connection);
  const Parent = createParentModel(connection);
  
  const main = async () => {
    const filteredParents: ParentInstance[] = [];
  
    // Working example
    const workingParents = await Parent.find().populate('child');
    if (workingParents.length > 0) {
      console.log(workingParents[0].fullName); // Here I can use fullName virtual variable
    }
  
    // Non-working example
    const parents = await Parent.find().populate<{ child: ChildInstance }>(
      'child'
    );
    parents.forEach((parent) => {
      const condition = parent.fullName === 'fake condition'; // error
      if (condition) {
        filteredParents.push(parent); // error
      }
    });
  };
  
  main();
  

@vkarpov15 vkarpov15 modified the milestones: 8.9.2, 8.9.3 Dec 18, 2024
vkarpov15 added a commit that referenced this issue Dec 30, 2024
types(model+query): avoid stripping out virtuals when calling populate with paths generic
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
confirmed-bug We've confirmed this is a bug in Mongoose and will fix it. typescript Types or Types-test related issue / Pull Request
Projects
None yet
3 participants