Search code examples
typescriptmongoosenestjstypescript-genericsnestjs-mongoose

Mongoose, how to enforce LeanDocument type?


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?


Solution

  • 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.