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 · 4 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?

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