I'm using mongoose and TypeScript in my Node.JS app. I'm using mongoose's populate
in a bunch of places when fetching data from the database.
The issue I'm facing is that I don't know how to type my models so that a property can be either an ObjectId or populated with data from another collection.
I've attempted using union types in my model type definition, which seems like something that TypeScript offers to cover these kind of things:
interface User extends Document { _id: Types.ObjectId; name: string } interface Item extends Document { _id: Types.ObjectId; // Union typing here user: Types.ObjectId | User; }
My schema only defines the property as an ObjectId with ref.
const ItemSchema = new Schema({ user: { type: Schema.Types.ObjectId, ref: "User", index: true } })
Example:
So I might do something like this:
ItemModel.findById(id).populate("user").then((item: Item) => { console.log(item.user.name); })
Which produces the compilation error:
[ts] Property 'name' does not exist on type 'User | ObjectId'. Property 'name' does not exist on type 'ObjectId'.
How can I have a model property that can be either of two types in TypeScript?
Mongoose Populate() Method. In MongoDB, Population is the process of replacing the specified path in the document of one collection with the actual document from the other collection.
Mongoose Schema vs. Model. A Mongoose model is a wrapper on the Mongoose schema. A Mongoose schema defines the structure of the document, default values, validators, etc., whereas a Mongoose model provides an interface to the database for creating, querying, updating, deleting records, etc.
Mongoose never create any collection until you will save/create any document.
mongoose. model() returns a Model ( It is a constructor, compiled from Schema definitions).
You need to use a type guard to narrow the type from Types.ObjectId | User
to User
...
If you are dealing with a User
class, you can use this:
if (item.user instanceof User) { console.log(item.user.name); } else { // Otherwise, it is a Types.ObjectId }
If you have a structure that matches a User
, but not an instance of a class (for example if User
is an interface), you'll need a custom type guard:
function isUser(obj: User | any) : obj is User { return (obj && obj.name && typeof obj.name === 'string'); }
Which you can use with:
if (isUser(item.user)) { console.log(item.user.name); } else { // Otherwise, it is a Types.ObjectId }
If you don't want to check structures for this purpose, you could use a discriminated union.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With