4
votes

I'm working on a project and need to retrieve specific subdocuments from a model by their subdocument _id's. I then plan on making updates to those subdocuments and saving the main document. The mongoose subdocument documentation lists a number of methods you can call on the parent.children array, but methods that don't already exist in Javascript for arrays give an error saying they do not exist and it doesn't compile. I'm referencing this documentation: https://mongoosejs.com/docs/subdocs.html

I understand that should be able to use .findOneAndUpdate to make my updates, and using the runValidators option everything should still be validated, but I would also like to just retrieve the subdocument itself as well.

I looked at this post: MongoDB, Mongoose: How to find subdocument in found document? , and I will comment that the answer is incorrect that if a subdocument schema is registered it automatically creates a collection for that schema, the collection is only made if that schema is saved separately. You cannot use ChildModel.findOne() and retrieve a subdocument, as the collection does not exist, there is nothing in it.

Having IChildModel extend mongoose.Types.Subdocument and having the IParent interface reference that instead of IChild and not registering the ChildModel does not change anything other than no longer allowing calls to .push() to not accept simple objects (missing 30 or so properties). Also trying mongoose.Types.Array<IChild> in the IParent interface with this method does not change anything.

Changing the IParent interface to use mongoose.Types.Array<IChild> for the children property allows addToSet() to work, but not id() or create()

I'm using Mongoose version 5.5.10, MongoDb version 4.2.0 and Typescript version 3.4.5

import mongoose, { Document, Schema } from "mongoose";

// Connect to mongoDB with mongoose
mongoose.connect(process.env.MONGO_HOST + "/" + process.env.DB_NAME, {useNewUrlParser: true, useFindAndModify: false});

// Interfaces to be used throughout application
interface IParent {
    name: string;
    children: IChild[];
}

interface IChild {
    name: string;
    age: number;
}

// Model interfaces which extend Document
interface IParentModel extends IParent, Document { }
interface IChildModel extends IChild, Document { }

// Define Schema
const Child: Schema = new Schema({
    name: {
        type: String,
        required: true
    },
    age: {
        type: Number,
        required: true
    }
});

const ChildSchema: Schema = Child;

const Parent: Schema = new Schema({
    name: {
        type: String,
        required: true
    },
    children: [ChildSchema]
});

const ParentSchema: Schema = Parent;

// Create the mongoose models
const ParentModel = mongoose.model<IParentModel>("Parent", Parent);
const ChildModel = mongoose.model<IChildModel>("Child", Child);

// Instantiate instances of both models
const child = new ChildModel({name: "Lisa", age: 7});
const parent = new ParentModel({name: "Steve", children: [child]});

const childId = child._id;

// Try to use mongoose subdocument methods
const idRetrievedChild = parent.children.id(childId); // Property 'id' does not exist on type 'IChild[]'.ts(2339)
parent.children.addToSet({ name: "Liesl", age: 10 }); // Property 'addToSet' does not exist on type 'IChild[]'.ts(2339)
parent.children.create({ name: "Steve Jr", age: 2 }); // Property 'create' does not exist on type 'IChild[]'.ts(2339)

// If I always know the exact position in the array of what I'm looking for
const arrayRetrievedChild = parent.children[0]; // no editor errors
parent.children.unshift(); // no editor errors
parent.children.push({ name: "Emily", age: 18 }); // no editor errors
2

2 Answers

10
votes

Kind of a late response, but I looked through the typings and found the DocumentArray

import { Document, Embedded, Types } from 'mongoose';
interface IChild extends Embedded {
    name: string;
}

interface IParent extends Document {
    name: string;
    children: Types.DocumentArray<IChild>;
}

Just wanted to put this here incase anyone else needs it.

2
votes

Gross: For now, I'm going with a very quick and dirty polyfill solution that doesn't actually answer my question:

declare module "mongoose" {
    namespace Types {
        class Collection<T> extends mongoose.Types.Array<T> {
            public id: (_id: string) => (T | null);
        }
    }
}

then we declare IParent as such:

interface IParent {
    name: string;
    children: mongoose.Types.Collection<IChild>;
}

Because the function id() already exists and typescript just doesn't know about it, the code works and the polyfill lets it compile.

Even Grosser: Otherwise for an even quicker and dirtier solution, when you create the parent model instance simply typecast it to any and throw out all typescript checks:

const child = new ChildModel({name: "Lisa", age: 7});
const parent: any = new ParentModel({name: "Steve", children: [child]});
const idRetrievedChild = parent.children.id(childId); // works because declared any