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

How to write a generic function to resolve PopulatedDoc type variables? #15121

Open
1 task done
nikzanda opened this issue Dec 19, 2024 · 7 comments
Open
1 task done
Labels
help This issue can likely be resolved in GitHub issues. No bug fixes, features, or docs necessary

Comments

@nikzanda
Copy link

Prerequisites

  • I have written a descriptive issue title

Mongoose version

8.9.1

Node.js version

20.10.0

MongoDB version

6.0.2

Operating system

macOS

Operating system version (i.e. 20.04, 11.3, 10)

No response

Issue

In my project, I often find myself having to implement this code:

// Check if 'myPopulatedDocProp' has already been populated in a previous part of the code
if (myDocument.myPopulatedDocProp instanceof AnotherModel) {
  return myDocument.myPopulatedDocProp;
}

// If 'myPopulatedDocProp' was not populated, perform a database query to find and populate the document property
const result = await AnotherModel
  .findById(myDocument.myPopulatedDocProp) // Query the AnotherModel to find the document by ID
  .orFail(); // Throw an error if the document is not found
return result; // Return the populated document property

Is it possible to write a generic function so that I don't have to rewrite the same exact code but with different models?
I tried with this code but it doesn't work:

export const findOrReturnInstance = async <T extends Document>(
  populatedDoc: PopulatedDoc<T>,
  model: Model<T>,
) => {
  if (populatedDoc instanceof model) {
    return populatedDoc;
  }
  const result = await model
    .findById(populatedDoc)
    .orFail();
  return result;
};

Is it the right approach or am I doing something wrong?
Attention, with this generic function I want to ensure that any virtual variables and instance methods, etc., are preserved.

Thank you for your help. 🙏🏻

@nikzanda nikzanda added help This issue can likely be resolved in GitHub issues. No bug fixes, features, or docs necessary help wanted labels Dec 19, 2024
@vkarpov15 vkarpov15 added this to the 8.9.4 milestone Dec 27, 2024
@vkarpov15
Copy link
Collaborator

The approach is reasonable, you just need to type model correctly. I would also recommend against using PopulatedDoc<> type for reasons explained here.

Take a look at the following script, would this approach work for you?

import mongoose, { Schema } from 'mongoose';

const ParentModel = mongoose.model('Parent', new Schema({
  child: { type: Schema.Types.ObjectId, ref: 'Child' },
  name: String
}));

const childSchema: Schema = new Schema({ name: String });
const ChildModel = mongoose.model('Child', childSchema);
type ChildHydratedDocType = ReturnType<(typeof ChildModel)['hydrate']>;

(async function() {
  const doc = await ParentModel.findOne({}).populate<{ child: ChildHydratedDocType }>('child').orFail();
  const res1: ChildHydratedDocType = await findOrReturnInstance(doc.child, ChildModel);

  const doc2 = await ParentModel.findOne().orFail();
  const res2: ChildHydratedDocType = await findOrReturnInstance(doc.child, ChildModel);
})();

async function findOrReturnInstance<HydratedDocType extends mongoose.Document>(
  docOrId: HydratedDocType | mongoose.Types.ObjectId,
  Model: mongoose.Model<any, any, any, any, HydratedDocType>
) {
  if (docOrId instanceof mongoose.Document) {
    return docOrId;
  }
  
  return Model.findById(docOrId).orFail();
}  

@vkarpov15 vkarpov15 removed this from the 8.9.4 milestone Jan 6, 2025
@nikzanda
Copy link
Author

nikzanda commented Jan 8, 2025

The script is fine, although I would have preferred the type to be inferred already by findOrReturnInstance and not specified manually.

I have another question: if I avoid using PopulatedDoc, how do I handle the following situation?

function fn(p: ParentInstance) {
   // TODO
}

const parent = await Parent.findOne({}).populate<{ child: ChildInstance }>('child').orFail();

// There appears to be a compilation error here, because if I populate child, it will no longer be of type ObjectId.
fn(parent); 

@vkarpov15
Copy link
Collaborator

Can you please elaborate on "although I would have preferred the type to be inferred already by findOrReturnInstance and not specified manually."?

Re: PopulatedDoc, that seems like expected behavior because parent is no longer strictly the same type as ParentInstance. You can always do something like fn(p: ParentInstance & { child: ChildInstance }) if you want to allow populated docs to be passed into fn().

@nikzanda
Copy link
Author

nikzanda commented Jan 9, 2025

Let me explain better: in the example you wrote, for res1 and res2 you explicitly declared their types. Do you think it is possible to make the findOrReturnInstance function return the correct type without having to specify it?

@vkarpov15
Copy link
Collaborator

Yeah the script compiles fine if you remove : ChildHydratedDocType from const res2: ChildHydratedDocType. I added the explicit type to demonstrate that the function's return type was correct.

@nikzanda
Copy link
Author

Yes, the script compiles fine, but if you remove the types from the res1 and res2 variables, they are typed as any. Also, the findOrReturnInstance function's return type is Promise<any>.

Reproduction link here

Image
Image

Do you think there's a way to make the function return the correct type without having to specify it manually each time?

@vkarpov15 vkarpov15 added this to the 8.9.6 milestone Jan 14, 2025
@vkarpov15 vkarpov15 added has repro script There is a repro script, the Mongoose devs need to confirm that it reproduces the issue and removed help This issue can likely be resolved in GitHub issues. No bug fixes, features, or docs necessary labels Jan 14, 2025
@vkarpov15
Copy link
Collaborator

How about the following? I explicitly set the return type on findOrReturnInstance, and also removed the : Schema type definition on childSchema that was causing TS to infer ChildModel as { [key: string]: unknown }

import mongoose, { Schema } from 'mongoose';

const ParentModel = mongoose.model('Parent', new Schema({
  child: { type: Schema.Types.ObjectId, ref: 'Child' },
  name: String
}));

const childSchema = new Schema({ name: String });
const ChildModel = mongoose.model('Child', childSchema);
type ChildHydratedDocType = ReturnType<(typeof ChildModel)['hydrate']>;

(async function() {
  const doc = await ParentModel.findOne({}).populate<{ child: ChildHydratedDocType }>('child').orFail();
  const res1 = await findOrReturnInstance(doc.child, ChildModel);

  const doc2 = await ParentModel.findOne().orFail();
  const res2 = await findOrReturnInstance(doc.child, ChildModel);

  const name1 = res1.name;
  const name2 = res2.name;
  f2(res1, name1);
  f2(res2, name2);
})();

async function findOrReturnInstance<HydratedDocType extends mongoose.Document>(
  docOrId: HydratedDocType | mongoose.Types.ObjectId,
  Model: mongoose.Model<any, any, any, any, HydratedDocType>
): Promise<HydratedDocType> {
  if (docOrId instanceof Model) {
    return docOrId;
  }

  return Model.findById(docOrId).orFail();
}

function f2(doc: ChildHydratedDocType, name?: string | null) {
  console.log(doc.name, name, doc.save());
}

Compiles correctly, and my editor (zed@0.166.2) seems to pick up correct type as shown in the following screenshot

Image

@vkarpov15 vkarpov15 added help This issue can likely be resolved in GitHub issues. No bug fixes, features, or docs necessary and removed has repro script There is a repro script, the Mongoose devs need to confirm that it reproduces the issue labels Jan 17, 2025
@vkarpov15 vkarpov15 removed this from the 8.9.6 milestone Jan 17, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
help This issue can likely be resolved in GitHub issues. No bug fixes, features, or docs necessary
Projects
None yet
Development

No branches or pull requests

2 participants