Handling Subdocuments in TypeScript

Subdocuments are tricky in TypeScript. By default, Mongoose treats object properties in document interfaces as nested properties rather than subdocuments.

// Setupimport { Schema, Types, model, Model } from'mongoose'; // Subdocument definitioninterfaceNames { _id: Types.ObjectId; firstName: string; } // Document definitioninterfaceUser { names: Names; } // Models and schemastypeUserModelType = Model<User>; const userSchema = newSchema<User, UserModelType>({ names: newSchema<Names>({ firstName: String }) }); constUserModel = model<User, UserModelType>('User', userSchema); // Create a new document:const doc = newUserModel({ names: { _id: '0'.repeat(24), firstName: 'foo' } }); // "Property 'ownerDocument' does not exist on type 'Names'."// Means that `doc.names` is not a subdocument! doc.names.ownerDocument();

Mongoose provides a mechanism to override types in the hydrated document. Define a separate THydratedDocumentType and pass it as the 5th generic param to mongoose.Model<>. THydratedDocumentType controls what type Mongoose uses for "hydrated documents", that is, what await UserModel.findOne(), UserModel.hydrate(), and new UserModel() return.

import { HydratedSingleSubdocument } from'mongoose'; // Define property overrides for hydrated documentstypeTHydratedUserDocument = { names?: HydratedSingleSubdocument<Names> } typeUserModelType = mongoose.Model<User, {}, {}, {}, THydratedUserDocument>; const userSchema = new mongoose.Schema<User, UserModelType>({ names: new mongoose.Schema<Names>({ firstName: String }) }); constUserModel = mongoose.model<User, UserModelType>('User', userSchema); const doc = newUserModel({ names: { _id: '0'.repeat(24), firstName: 'foo' } }); doc.names!.ownerDocument(); // Works, `names` is a subdocument! doc.names!.firstName; // 'foo'

Subdocument Arrays

You can also override arrays to properly type subdocument arrays using TMethodsAndOverrides:

// Subdocument definitioninterfaceNames { _id: Types.ObjectId; firstName: string; } // Document definitioninterfaceUser { names: Names[]; } // TMethodsAndOverridestypeTHydratedUserDocument = { names?: Types.DocumentArray<Names> } typeUserModelType = Model<User, {}, {}, {}, THydratedUserDocument>; // Create modelconstUserModel = model<User, UserModelType>('User', newSchema<User, UserModelType>({ names: [newSchema<Names>({ firstName: String })] })); const doc = newUserModel({}); doc.names[0].ownerDocument(); // Works! doc.names[0].firstName; // string
close