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);
})