Mongoose ObjectId, The Small Detail I Didn’t Know

Ángel Cereijo
3 min readJan 19, 2024

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.

--

--

Ángel Cereijo

Software engineer with 15 year of experience. Lately focused in Node.js ecosystem and good practices.