-
-
Notifications
You must be signed in to change notification settings - Fork 3.8k
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
Intersection type error in embedded schema when iterating through an embedded array #14793
Closed
2 tasks done
Labels
confirmed-bug
We've confirmed this is a bug in Mongoose and will fix it.
typescript
Types or Types-test related issue / Pull Request
Comments
IslandRhythms
added
confirmed-bug
We've confirmed this is a bug in Mongoose and will fix it.
typescript
Types or Types-test related issue / Pull Request
labels
Aug 7, 2024
import * as mongoose from 'mongoose';
interface IComment {
userName: string;
userSurname: string;
body: string;
createdAt: Date;
updatedAt: Date;
}
interface ICommentVirtuals {
user: string;
}
type CommentModelType = mongoose.Model<IComment, {}, {}, ICommentVirtuals>;
type CommentInstance = mongoose.Types.Subdocument<mongoose.Types.ObjectId> &
IComment &
ICommentVirtuals;
const options: mongoose.SchemaOptions<IComment> = {
_id: false,
timestamps: true,
};
export const commentSchema = new mongoose.Schema<IComment, CommentModelType>(
{
userName: {
type: mongoose.SchemaTypes.String,
required: true,
trim: true,
},
userSurname: {
type: mongoose.SchemaTypes.String,
required: true,
trim: true,
},
body: {
type: mongoose.SchemaTypes.String,
required: true,
trim: true,
},
},
options
);
commentSchema.virtual('user').get(function () {
const result = `${this.userName} ${this.userSurname}`;
return result;
});
interface IDetail {
comments: IComment[];
}
type DetailDocumentOverrides = {
comments: CommentInstance[];
};
type DetailModelType = mongoose.Model<IDetail, {}, DetailDocumentOverrides>;
type DetailInstance = mongoose.Types.Subdocument<mongoose.Types.ObjectId> &
IDetail &
DetailDocumentOverrides;
const options2: mongoose.SchemaOptions<IDetail> = {
_id: false,
};
const detailSchema = new mongoose.Schema<IDetail, DetailModelType>(
{
comments: {
type: [commentSchema],
default: [],
},
}, options2
);
interface IPost {
_id: mongoose.Types.ObjectId;
title: string;
body: string;
detail: IDetail;
createdAt: Date;
updatedAt: Date;
}
type PostDocumentOverrides = {
detail: DetailInstance;
};
interface IPostVirtuals {
id: string;
}
type PostModelType = mongoose.Model<IPost, {}, PostDocumentOverrides, IPostVirtuals>;
type PostInstance = InstanceType<PostModelType>;
const options3: mongoose.SchemaOptions<IPost> = {
timestamps: true,
optimisticConcurrency: true,
};
const postSchema = new mongoose.Schema<IPost, PostModelType>(
{
title: {
type: mongoose.SchemaTypes.String,
required: true,
trim: true,
},
body: {
type: mongoose.SchemaTypes.String,
required: true,
trim: true,
},
detail: {
type: detailSchema,
required: true,
},
},
options3
);
postSchema.index({ title: 1 });
const main = async () => {
const connection = await mongoose.connect('mongodb://localhost:27017')
const PostModel = mongoose.model<IPost, PostModelType>('Post', postSchema);
await PostModel.create({
title: 'title',
body: 'post body',
detail: {
comments: [
{
userName: 'name',
userSurname: 'surname',
body: 'comment body',
},
],
},
});
const post = await PostModel.findOne();
if (post) {
console.log('post.detail.comments[0].user:', post.detail.comments[0].user);
post.detail.comments.forEach((comment) => {
console.log(comment.user);
});
post.detail.comments.forEach((comment: CommentInstance) => {
console.log(comment.user);
});
}
};
main(); |
Here's the complete script that compiles successfully with #14800 . The major changes are:
There's some more examples of how this works in the TypeScript schema docs import * as mongoose from 'mongoose';
interface IComment {
userName: string;
userSurname: string;
body: string;
createdAt: Date;
updatedAt: Date;
}
interface ICommentVirtuals {
user: string;
}
type CommentInstance = mongoose.HydratedSingleSubdocument<IComment, ICommentVirtuals>;
type CommentModelType = mongoose.Model<IComment, {}, {}, ICommentVirtuals, CommentInstance>;
const options: mongoose.SchemaOptions<IComment> = {
_id: false,
timestamps: true,
};
export const commentSchema = new mongoose.Schema<IComment, CommentModelType, {}, {}, {}, {}, mongoose.SchemaOptions<IComment>, IComment, CommentInstance>(
{
userName: {
type: mongoose.SchemaTypes.String,
required: true,
trim: true,
},
userSurname: {
type: mongoose.SchemaTypes.String,
required: true,
trim: true,
},
body: {
type: mongoose.SchemaTypes.String,
required: true,
trim: true,
},
},
options
);
commentSchema.virtual('user').get(function () {
const result = `${this.userName} ${this.userSurname}`;
return result;
});
interface IDetail {
comments: IComment[];
}
type DetailDocumentOverrides = {
comments: CommentInstance[];
};
type DetailInstance = mongoose.HydratedSingleSubdocument<IDetail, DetailDocumentOverrides>;
type DetailModelType = mongoose.Model<IDetail, {}, DetailDocumentOverrides, DetailInstance>;
const options2: mongoose.SchemaOptions<IDetail> = {
_id: false,
};
const detailSchema = new mongoose.Schema<IDetail, DetailModelType, {}, {}, {}, {}, mongoose.SchemaOptions<IDetail>, IDetail, DetailInstance>(
{
comments: {
type: [commentSchema],
default: [],
},
}, options2
);
interface IPost {
_id: mongoose.Types.ObjectId;
title: string;
body: string;
detail: IDetail;
createdAt: Date;
updatedAt: Date;
}
type PostDocumentOverrides = {
detail: DetailInstance;
};
interface IPostVirtuals {
id: string;
}
type PostInstance = mongoose.HydratedDocument<IPost, PostDocumentOverrides>;
type PostModelType = mongoose.Model<IPost, {}, {}, IPostVirtuals, PostInstance>;
const options3: mongoose.SchemaOptions<IPost> = {
timestamps: true,
optimisticConcurrency: true,
};
const postSchema = new mongoose.Schema<IPost, PostModelType, {}, {}, IPostVirtuals, {}, mongoose.SchemaOptions<IPost>, IPost, PostInstance>(
{
title: {
type: mongoose.SchemaTypes.String,
required: true,
trim: true,
},
body: {
type: mongoose.SchemaTypes.String,
required: true,
trim: true,
},
detail: {
type: detailSchema,
required: true,
},
},
options3
);
postSchema.index({ title: 1 });
const main = async () => {
const connection = await mongoose.connect('mongodb://localhost:27017')
const PostModel = mongoose.model<IPost, PostModelType>('Post', postSchema);
await PostModel.create({
title: 'title',
body: 'post body',
detail: {
comments: [
{
userName: 'name',
userSurname: 'surname',
body: 'comment body',
},
],
},
});
const post = await PostModel.findOne().orFail();
if (post) {
console.log('post.detail.comments[0].user:', post.detail.comments[0].user);
post.detail.comments.forEach((comment) => {
console.log(comment.user);
});
post.detail.comments.forEach((comment: CommentInstance) => {
console.log(comment.user);
});
}
};
main(); |
Okay, as soon as the PR is merged, I will try to type it this way, thanks! |
vkarpov15
added a commit
that referenced
this issue
Aug 13, 2024
types: make HydratedSingleSubdocument and HydratedArraySubdocument merge types instead of using &
This was referenced Sep 4, 2024
This was referenced Sep 19, 2024
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Labels
confirmed-bug
We've confirmed this is a bug in Mongoose and will fix it.
typescript
Types or Types-test related issue / Pull Request
Prerequisites
Mongoose version
8.5.2
Node.js version
20.10.0
MongoDB server version
6.0.2
Typescript version (if applicable)
5.5.4
Description
I have a complex Mongoose schema: my model has an embedded field which in turn has an array of embedded fields. This last field contains a virtual field.
In the embedded detail, I declare comments in
IDetail
asIComment[]
, and in the typeDetailDocumentOverrides
, I declare comments asCommentInstance[]
. CommentInstance is of type:Types.Subdocument<Types.ObjectId> & IComment & ICommentVirtuals
.There is a difference in typing when I access a comment directly or when I iterate over them, for example:
post.detail.comments[0]
is of typeCommentInstance
, so I can access the virtual field; however, if I iterate:post.detail.comments.forEach((comment) => { … })
, the variable comment is of typeIComment
, but this is not entirely correct, because comment actually contains all the other properties hydrated by Mongoose.The actual type generated for the comments field is
IComment[] & CommentInstance[]
, but it’s not a good idea to generate types this way, as suggested by an active member of the TypeScript team (stackoverflow comment, github issue).Am I wrong in typing the embedded fields? In this case, feel free to correct my code.
In the following screen, TypeScript does not return an error in the first line, while it does inside the forEach (“user” is a virtual field):
A workaround is to type the comment variable inside the forEach as CommentInstance.
However, I wanted to understand if this is a problem that can be solved in some way, if I will always have to type manually in these cases, or if I am wrong in typing the embedded fields.
Steps to Reproduce
Reproduction link: stackblitz
Expected Behavior
No response
The text was updated successfully, but these errors were encountered: