4

In our codebase we've been using T.lean() or T.toObject() and our return types would be LeanDocument<T>. Mongoose 7 no longer exports LeanDocument, and the existing migration guide suggests using the following setup:

// Do this instead, no `extends Document`
interface ITest {
  name?: string;
}
const Test = model<ITest>('Test', schema);

// If you need to access the hydrated document type, use the following code
type TestDocument = ReturnType<(typeof Test)['hydrate']>;

But this gives me HydratedDocument that I can get by HydratedDocument<T>, which is not what I want since it has all the document methods on it.
As an alternative I can use just T as my return type, but then any Document<T> is matching T.

I'd like to enforce that the result is a POJO, to prevent documents leaking from our DAL.

How can I achieve that with typescript and mongoose types?

EcksDy
  • 1,289
  • 9
  • 25

1 Answers1

1

Asking a similar question over at the mongoose repo, I've settled on the following approach:

// utils.ts
export type LeanDocument<T> = T & { $locals?: never };

So in the following case, typescript will remind me that I cannot return document:

async function getById(id: string): Promise<LeanDocument<User>> {
  const user = await UserModel.findById(id);
  return user;
  //       ^ Types of property '$locals' are incompatible.
}

I think this can be further improved by making a clearer type error that will state something along the lines of Type error ... "You've forgot to convert to a lean document"., as I've seen that in libraries before.
But I haven't found how to do that yet :)

Edit

Some typescript magic:

export type LeanDocument<T> = T & T extends { $locals: never }
  ? T
  : 'Please convert the document to a plain object via `.toObject()`';

Will result in the following error:

async function getById(id: string): Promise<LeanDocument<User>> {
  const user = await UserModel.findById(id);
  return user;
  //       ^ Type 'Document<unknown, any, User> & Omit<User & { _id: ObjectId; }, never>'
  // is not assignable to type 
  // '"Please convert the document to a plain object via `.toObject()`"'.ts(2322)
}

Edit 2

The type error using conditional types did not work as expected and I've tried to solve it in this question. Unfortunately the working solution required a wrapping function and assertion.

EcksDy
  • 1,289
  • 9
  • 25
  • Hi @EcksDy, were you able to extract POJO from mongoose document? if yes, can you please share your solution – Himesh Jul 20 '23 at 07:29
  • You can use `doc.toObject()` or `doc.toJSON()` to get a POJO instead of a document. The purpose of the question was to see how can I enforce conversion to POJO via the type system. – EcksDy Jul 20 '23 at 07:51
  • yes, i am also trying to achieve POJO using type system. Already spent couple of days without any luck. I will use toObject or toJSON as last resort – Himesh Jul 20 '23 at 09:12
  • Look at the second edit, I've made a followup question on the topic. Just marking the return value as the type you expect is not enough, as in runtime you will have the full document returned. So while you won't be able to access document properties - they will still be there. That's why you still want to use `.toObject()` or `.toJSON()`, as these are the best ways to make sure you don't leak the actual document out. – EcksDy Jul 20 '23 at 12:37