How to Typescript mongoose toObject?

Viewed 731

What is the correct way of typing .toObject() return?

const userDoc = await UsersModel.findOne({email})
const user:IUserObject = userDoc.toObject()

The user is not IUserObject. Its typing doesn't make sense: Pick<Pick<_LeanDocument<IUserModel>, "_id"....

Here is my model typing:

import {Schema, model, Document} from 'mongoose'

export interface IUser {
  email: string
  password: string
}

export interface IUserObject extends IUser {
  _id: string
}

export interface IUserModel extends Document, IUser {}

const schema = new Schema({
  email: { type: String, required: true },
  password: { type: String, required: true }
})

export const UsersModel = model<IUserModel>('Users', schema)
2 Answers

Mongoose original type for doc.toObject() looks really convoluted and most likely comes from time before conditional types and infer operator. Even worse it seems to be incorrect representation of the runtime value, which according to mongoose docs should be just POJO:

type ToObjectReturnType = ReturnType<Document<IUser>['toObject']>
/*
  ToObjectReturnType = {
    _id?: IUser;
    __v?: number;
    id?: any;
  }
*/

A possible solution / improvement without fixing underlying problem in mongoose could be using an auxiliary function to infer underlying model from the document and make a call to doc.toObject:

// a generic method for infering underlying Model out of a mongoose Document
// and interecting it with { _id: string }
const docToObject = <D extends Document<any>>(doc: D) =>
  doc.toObject() as D extends Document<infer Model>
    ? Model & { _id: string } // not sure if _id is always there - please modify to your needs / real runtime value
    : never;

// example of usage
const findUserByEmail = async (email: string) => {
  const userDoc = await UsersModel.findOne({ email });
  return docToObject(userDoc);
};

STACKBLITZ

remark

Please mind - that according to mongoose docs - it is possible to configure .toObject on the schema level and/or pass params when calling .toObject. To properly type these use cases is out of scope of this question or not possible with current Typescript version altogether.

You could use Omit<> and replace the type declaration of toObject

export interface IUserModel extends Omit<Document, 'toObject'>, IUser {
      toObject: () => IUserObject;
}
Related