DEV Community

Cover image for Stop Writing Your Mongoose Schema Twice: InferSchemaType, HydratedDocument, and the populate() Fix Nobody Explains
Dev Encyclopedia
Dev Encyclopedia

Posted on Originally published at devencyclopedia.com

Stop Writing Your Mongoose Schema Twice: InferSchemaType, HydratedDocument, and the populate() Fix Nobody Explains

If you've added TypeScript to a Mongoose project, you've probably written the same shape twice, once as an interface, once as the schema that actually enforces it at runtime. They don't check each other. Add a field to the schema and forget the interface, and TypeScript won't catch it, because as far as the type checker knows, the interface is correct. It's just wrong relative to what actually gets saved to MongoDB.

InferSchemaType fixes that duplication, it derives the plain data shape straight from the schema. But it only gets you the data shape. It doesn't know about .save(), it doesn't know about ._id, and it has no idea what Mongoose attaches to a document once it comes back from a query. That's where HydratedDocument comes in, and skipping it is the single most common reason people end up reaching for "as any" on a perfectly normal query result.

Then there's the part that trips up almost everyone eventually: .populate(). By default a referenced field types as a raw ObjectId, even after you've populated it. TypeScript has no way to see that a runtime call changed the shape of your result. There's an open GitHub issue on Mongoose's own repo about exactly this, with no canonical fix linked anywhere easy to find.

I break down the full fix, plus typing instance methods, statics, virtuals, and when you should still hand-write an interface, here: https://devencyclopedia.com/blog/mongoose-typescript-interfaces

If you just want the interface and populate snippet generated for your actual schema instead of reconstructing the generics from memory, I also built a free browser tool for that: https://devencyclopedia.com/tools/mongoosets

Top comments (0)