mongoose: Populate type with TypeScript not working

Do you want to request a feature or report a bug? bug

What is the current behavior? property in interface with type PopulatedDoc<interface & Document> has type any If the current behavior is a bug, please provide the steps to reproduce.

import mongoose, { Schema, model, Document, PopulatedDoc } from 'mongoose';
async function run() {
    mongoose.connect('mongodb://localhost:27017/test');
    // `child` is either an ObjectId or a populated document
    interface Parent {
        child?: PopulatedDoc<Child & Document>,
        name?: string
    }
    const ParentModel = model<Parent>('Parent', new Schema<Parent>({
        child: { type: 'ObjectId', ref: 'Child' },
        name: String
    }));

    interface Child {
        name?: string;
    }
    const childSchema: Schema = new Schema<Child>({ name: String });
    const ChildModel = model<Child>('Child', childSchema);

    const parent = await ParentModel.findOne({}).populate('child')
    parent!.child.name;
}
{
  "compilerOptions": {
    "target": "ES6",                      
    "module": "commonjs",                     
    "strict": true,                          
    "esModuleInterop": true,                 
    "forceConsistentCasingInFileNames": true 
  }
}

What is the expected behavior? it should support the type of given interface What are the versions of Node.js, Mongoose and MongoDB you are using? Note that “latest” is not a version. Node.js: 14.17.4 , Mongoose: 6.0.7 , MongoDB: 4.4.3

About this issue

  • Original URL
  • State: closed
  • Created 3 years ago
  • Reactions: 4
  • Comments: 18 (6 by maintainers)

Most upvoted comments

@vkarpov15 I’m running in the same issue as @judgegodwins regarding the populate method returning an intersection type:

ParentModel.findOne({})
  .populate<{ child: Child }>('child')
  .orFail()
  .then((doc) => {
    const t: string = doc.child.name
  })

The type of doc.child in the callback is Types.ObjectId & Child, while we expect Child | null. (iirc the null type should be automatically added since Mongoose might return it if the document was not found).

Even when using .populate<{ child: Child | null }>('child'), doc.child stays Types.ObjectId & Child.

Typescript 4.2.4 / Mongoose 6.0.13

@thiagokisaki using your example above, there’s still no name property on child. image

@dantenol please open a new issue and follow the issue template

Hi, this isn’t about any compilation issues, it’s just the actual behavior of the populate regarding types doesn’t seem to be expected behavior:

The type of doc.child in the callback is Types.ObjectId & Child, while we expect Child | null. (iirc the null type should be automatically added since Mongoose might return it if the document was not found).

Even when using .populate<{ child: Child | null }>('child'), doc.child stays Types.ObjectId & Child.

Am I misunderstanding how this should work or is there something not making sense to you as well ?

@thiagokisaki we made some improvements to make this easier by passing a generic param to populate() as shown below.

import { Schema, model, Document, Types } from 'mongoose';

// `Parent` represents the object as it is stored in MongoDB
interface Parent {
  child?: Types.ObjectId,
  name?: string
}
interface Child {
  name: string;
}
// `PopulatedParent` represents the possible populated paths
interface PopulatedParent {
  child: Child | null;
}
const ParentModel = model<Parent>('Parent', new Schema({
  child: { type: 'ObjectId', ref: 'Child' },
  name: String
}));
const childSchema: Schema = new Schema({ name: String });
const ChildModel = model<Child>('Child', childSchema);

// Populate with `Paths` generic `{ child: Child }` to override `child` path
ParentModel.findOne({}).populate<Pick<PopulatedParent, 'child'>>('child').orFail().then(doc => {
  // Works
  const t: string = doc.child.name;
});