Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 63 additions & 0 deletions test/types/populate.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -420,6 +420,69 @@ function gh14441() {
const docObject = docs[0]!.toObject();
ExpectType<string>(docObject.child.name);
});

interface ArrayParent {
children?: Types.ObjectId[];
title?: string;
}
const ArrayParentModel = model<ArrayParent>(
'ArrayParent',
new Schema({
title: String,
children: [{ type: Schema.Types.ObjectId, ref: 'Child' }]
})
);

type PopulatedChildren = {
children: mongoose.Types.DocumentArray<mongoose.HydratedDocFromModel<typeof ChildModel>>;
};

ArrayParentModel.findOne({})
.orFail()
.then(async doc => {
const populatedDoc = await doc.populate<PopulatedChildren>('children');

// Populating changes the path type from ObjectIds to documents, so the result should
// not be assignable back to the model's original hydrated type.
// @ts-expect-error Type 'PopulateDocumentResult<Document<unknown, {}, ArrayParent, {}, DefaultSchemaOptions> & ArrayParent...'
const hydratedDoc: mongoose.HydratedDocFromModel<typeof ArrayParentModel> = populatedDoc;
ExpectAssignable<Document<any>>()(populatedDoc);
ExpectAssignable<Document<unknown>>()(populatedDoc);
ExpectType<string | undefined>(populatedDoc.children[0]?.name);
const plainObject = populatedDoc.toObject();
ExpectType<string>(plainObject.children[0].name);
const depopulatedObject = populatedDoc.toObject({ depopulate: true });
ExpectType<Types.ObjectId>(depopulatedObject.children![0]);
});

ArrayParentModel.findOne({})
.populate<PopulatedChildren>('children')
.orFail()
.then(populatedDoc => {
ExpectAssignable<Document<any>>()(populatedDoc);
ExpectAssignable<Document<unknown>>()(populatedDoc);
ExpectType<string | undefined>(populatedDoc.children[0]?.name);
const plainObject = populatedDoc.toObject();
ExpectType<string>(plainObject.children[0].name);
const depopulatedObject = populatedDoc.toObject({ depopulate: true });
ExpectType<Types.ObjectId>(depopulatedObject.children![0]);
Comment on lines +451 to +468
Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This hunk adds type assertions for toObject() and toObject({ depopulate: true }) on populated docs, but the PR also changes toJSON() typings. To prevent regressions, add equivalent assertions for toJSON() and toJSON({ depopulate: true }) on these populated results (doc.populate, query.populate, and Model.populate cases).

Copilot uses AI. Check for mistakes.
});

ArrayParentModel.findOne({})
.orFail()
.then(async doc => {
const populatedDoc = await ArrayParentModel.populate<PopulatedChildren>(doc, 'children');

// @ts-expect-error Type 'PopulateDocumentResult<Document<unknown, {}, ArrayParent, {}, DefaultSchemaOptions> & ArrayParent...'
const hydratedDoc: mongoose.HydratedDocFromModel<typeof ArrayParentModel> = populatedDoc;
ExpectAssignable<Document<any>>()(populatedDoc);
ExpectAssignable<Document<unknown>>()(populatedDoc);
ExpectType<string | undefined>(populatedDoc.children[0]?.name);
const plainObject = populatedDoc.toObject();
ExpectType<string>(plainObject.children[0].name);
const depopulatedObject = populatedDoc.toObject({ depopulate: true });
ExpectType<Types.ObjectId>(depopulatedObject.children![0]);
});
}

async function gh14574() {
Expand Down
6 changes: 3 additions & 3 deletions types/document.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ declare module 'mongoose' {
_id: T;

/** Assert that a given path or paths is populated. Throws an error if not populated. */
$assertPopulated<Paths = {}>(path: string | string[], values?: Partial<Paths>): Omit<this, keyof Paths> & Paths;
$assertPopulated<Paths = {}>(path: string | string[], values?: Partial<Paths>): PopulateDocumentResult<this, Paths, PopulatedPathsDocumentType<DocType, Paths>, DocType, TVirtuals, TSchemaOptions>;

/** Clear the document's modified paths. */
$clearModifiedPaths(): this;
Expand Down Expand Up @@ -238,8 +238,8 @@ declare module 'mongoose' {
$parent(): Document | undefined;

/** Populates document references. */
populate<Paths = {}>(path: string | PopulateOptions | (string | PopulateOptions)[]): Promise<MergeType<this, Paths>>;
populate<Paths = {}>(path: string, select?: string | AnyObject, model?: Model<any>, match?: AnyObject, options?: PopulateOptions): Promise<MergeType<this, Paths>>;
populate<Paths = {}>(path: string | PopulateOptions | (string | PopulateOptions)[]): Promise<PopulateDocumentResult<this, Paths, PopulatedPathsDocumentType<DocType, Paths>, DocType, TVirtuals, TSchemaOptions>>;
populate<Paths = {}>(path: string, select?: string | AnyObject, model?: Model<any>, match?: AnyObject, options?: PopulateOptions): Promise<PopulateDocumentResult<this, Paths, PopulatedPathsDocumentType<DocType, Paths>, DocType, TVirtuals, TSchemaOptions>>;

/** Gets _id(s) used during population of the given `path`. If the path was not populated, returns `undefined`. */
populated(path: string): any;
Expand Down
4 changes: 2 additions & 2 deletions types/models.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -628,10 +628,10 @@ declare module 'mongoose' {
populate<Paths>(
docs: Array<any>,
options: PopulateOptions | Array<PopulateOptions> | string
): Promise<Array<MergeType<THydratedDocumentType, Paths>>>;
): Promise<Array<PopulateDocumentResult<THydratedDocumentType, Paths, PopulatedPathsDocumentType<TRawDocType, Paths>, TRawDocType, TVirtuals>>>;
populate<Paths>(
doc: any, options: PopulateOptions | Array<PopulateOptions> | string
): Promise<MergeType<THydratedDocumentType, Paths>>;
): Promise<PopulateDocumentResult<THydratedDocumentType, Paths, PopulatedPathsDocumentType<TRawDocType, Paths>, TRawDocType, TVirtuals>>;

/**
* Update an existing [Atlas search index](https://www.mongodb.com/docs/atlas/atlas-search/create-index/).
Expand Down
28 changes: 28 additions & 0 deletions types/populate.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,34 @@ declare module 'mongoose' {
RawId extends RefType = (PopulatedType extends { _id?: RefType; } ? NonNullable<PopulatedType['_id']> : Types.ObjectId) | undefined
> = PopulatedType | RawId;

type PopulatedPathsDocumentType<RawDocType, Paths> = UnpackedIntersection<RawDocType, Paths>;

type PopulatedPathsSerializationReturnType<
PopulatedRawDocType,
DepopulatedRawDocType,
TVirtuals,
O extends ToObjectOptions,
TSchemaOptions = {}
> = O extends { depopulate: true }
? ToObjectReturnType<DepopulatedRawDocType, TVirtuals, O, TSchemaOptions>
: ToObjectReturnType<PopulatedRawDocType, TVirtuals, O, TSchemaOptions>;

type PopulateDocumentResult<
Doc,
Paths,
PopulatedRawDocType,
DepopulatedRawDocType = PopulatedRawDocType,
TVirtuals = {},
TSchemaOptions = {}
> = Omit<MergeType<Doc, Paths>, 'toJSON' | 'toObject'> & {
toJSON<O extends ToObjectOptions>(options: O): PopulatedPathsSerializationReturnType<PopulatedRawDocType, DepopulatedRawDocType, TVirtuals, O, TSchemaOptions>;
toJSON(options?: ToObjectOptions): Default__v<Require_id<PopulatedRawDocType>, TSchemaOptions>;
toJSON<T>(options?: ToObjectOptions): Default__v<Require_id<T>, ResolveSchemaOptions<TSchemaOptions>>;
toObject<O extends ToObjectOptions>(options: O): PopulatedPathsSerializationReturnType<PopulatedRawDocType, DepopulatedRawDocType, TVirtuals, O, TSchemaOptions>;
toObject(options?: ToObjectOptions): Default__v<Require_id<PopulatedRawDocType>, TSchemaOptions>;
toObject<T>(options?: ToObjectOptions): Default__v<Require_id<T>, ResolveSchemaOptions<TSchemaOptions>>;
};

interface PopulateOptions {
/** space delimited path(s) to populate */
path: string;
Expand Down
16 changes: 14 additions & 2 deletions types/query.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -201,10 +201,22 @@ declare module 'mongoose' {
? ResultType
: ResultType extends (infer U)[]
? U extends Document
? HydratedDocument<MergeType<RawDocType, Paths>, TDocOverrides, TQueryHelpers>[]
? PopulateDocumentResult<
HydratedDocument<MergeType<RawDocType, Paths>, TDocOverrides, TQueryHelpers>,
{},
MergeType<RawDocType, Paths>,
RawDocType,
TDocOverrides
>[]
: (MergeType<U, Paths>)[]
: ResultType extends Document
? HydratedDocument<MergeType<RawDocType, Paths>, TDocOverrides, TQueryHelpers>
? PopulateDocumentResult<
HydratedDocument<MergeType<RawDocType, Paths>, TDocOverrides, TQueryHelpers>,
{},
MergeType<RawDocType, Paths>,
RawDocType,
TDocOverrides
>
: MergeType<ResultType, Paths>
: MergeType<ResultType, Paths>;

Expand Down
Loading