TypeScript - How to define model in combination with using mongoose populate?
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.
Mongoose's TypeScript bindings export a PopulatedDoc type that helps you define populated documents in your TypeScript definitions:
import { Schema, model, Document, PopulatedDoc } from 'mongoose';
// `child` is either an ObjectId or a populated document
interface Parent {
child?: PopulatedDoc<Child & Document>,
name?: string
}
const ParentModel = model<Parent>('Parent', new Schema({
child: { type: 'ObjectId', ref: 'Child' },
name: String
}));
interface Child {
name?: string;
}
const childSchema: Schema = new Schema({ name: String });
const ChildModel = model<Child>('Child', childSchema);
ParentModel.findOne({}).populate('child').orFail().then((doc: Parent) => {
// Works
doc.child.name.trim();
})
Below is a simplified implementation of the PopulatedDoc type. It takes 2 generic parameters: the populated document type PopulatedType, and the unpopulated type RawId. RawId defaults to an ObjectId.
type PopulatedDoc<PopulatedType, RawId = Types.ObjectId> = PopulatedType | RawId;
You as the developer are responsible for enforcing strong typing between populated and non-populated docs. Below is an example.
ParentModel.findOne({}).populate('child').orFail().then((doc: Parent) => {
// `doc` doesn't have type information that `child` is populated
useChildDoc(doc.child);
});
// You can use a function signature to make type checking more strict.
function useChildDoc(child: Child): void {
console.log(child.name.trim());
}
This was lifted from the docs. You can check it here
You can use PopulatedDoc
type from the @types/mongoose
lib. See mongoose.doc.
Cast the item.user
to User
, when the user is populated.
ItemModel.findById(id).populate("user").then((item: Item) => {
console.log((<User>item.user).name);
})