Mongoose ObjectId, The Small Detail I Didn’t Know
I have been working with MongoDB and Mongoose for a few years. The other day, a coworker taught me something really simple that I didn’t know about the ObjectId type.
The scenario
I was working on a code in which I had an entity of a specific type. (BlogType)
// TS types
type AuthorType = {
_id: ObjectId;
name: string;
email: string;
}
type BlogType = {
_id: ObjectId;
title: string;
author: ObjectId | AuthorType;
body: string;
}
// Mongoose schemas
const AuthorSchema = new Schema({
name: String,
email: String,
});
const BlogSchema = new Schema({
title: String,
author: { type: Schema.Types.ObjectId, ref: 'author' },
body: String,
});
I needed to send the author value to another method. The expected parameter should be a string.
function logAuthorId(authorId: string) {
console.log('authorId', authorId);
}
As my author
property could be either an ObjectId or an ‘AuthorType’, it means that when I load the blog record, I can receive either the author ID or the complete author data. To send only the author ID to my logAuthorId
method, I wrote code like this:
logAuthorId(blog.author._id ? blog.author._id.toString() : blog.author.toString()
If it has a _id
property then I have an entity in the author
property. But if not, then my author
is the ObjectId, and I can use toString()
to obtain the ID as a string.
The finding
I was told that I didn’t need the ternary because even if my author
property is an ObjectId, there is an _id
property available (this is the thing I didn’t know), and my code can simply be:
logAuthorId(blog.author._id.toString())
And that’s true, as you can see in the image below, ObjectId
has its own _id
property.
But, if you go to the official mongoose doc, you can see the existing methods but nothing about the _id
property. You need to find it in types definition.
If you’re wondering why we have author
as ObjectId
or AuthorType
, it’s because, in some cases, I want to populate the author data in my blog record. This can be useful if you want the flexibility to handle IDs or entities in your code depending on each case
You also have the option to create two different types and, based on whether you use thepopulate
option or not, define that your method returns one type or the other.
type BlogType = {
_id: ObjectId;
title: string;
author: ObjectId;
body: string;
}
type BlogTypeWithAuthor = {
_id: ObjectId;
title: string;
author: AuthorType;
body: string;
}
Maybe the problem with doing this is that you may find that for some generic methods, you will need to indicate that your param can be one type or the other.
function genericMethod(blog: BlogType | BlogTypeWithAuthor) {...}
This is a record example of doing a find without populate
// code
const result = await Blog.findOne({
_id: blog._id,
})
.lean()
.populate('author') as BlogType;
// result
{
title: 'test',
author: new ObjectId('65a41f62ca7ea39009340473'),
body: 'test',
_id: new ObjectId('65a41f62ca7ea39009340475'),
__v: 0
}
Performing the query populating the author
property
// code
const result = await Blog.findOne({
_id: blog._id,
})
.lean()
.exec() as BlogType;
// result
{
_id: new ObjectId('65a41f62ca7ea39009340475'),
title: 'test',
author: {
_id: new ObjectId('65a41f62ca7ea39009340473'),
name: 'Arthur',
email: 'test@emai.com',
__v: 0
},
body: 'test',
__v: 0
}
Do you know about this in ObjectId
?
Thanks for reading :-)
Note
In my example code, I’ve used Type
as part of my type definitions but, please do not do this. It’s considered a “bad practice”, as it’s to use a I
for interfaces.
If you’re wondering why, here’s a “basic” explanation.
You are supposed to use Type
o I
to indicate people know whether it’s an interface or a type. However, what happens if you want, for example, to convert an interface into a type or vice versa? In that situation, you will need to change the name and update it in all places where you’re using your type or interface.
For more information about types and interfaces in TypeScript, you can check here.