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)