Mongoose, how to enforce LeanDocument type?
Asked Answered
Z

2

6

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?

Zed answered 11/5, 2023 at 11:49 Comment(0)
Z
2

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.

Zed answered 30/5, 2023 at 15:49 Comment(4)
Hi @EcksDy, were you able to extract POJO from mongoose document? if yes, can you please share your solutionShanleigh
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.Zed
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 resortShanleigh
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.Zed
C
0

The return type for lean documents in generel in mongoose 8 is:

(mongoose.FlattenMaps<unknown> & Required<{ _id: unknown; }>) | null
Chaliapin answered 13/10, 2024 at 12:44 Comment(0)

© 2022 - 2025 — McMap. All rights reserved.