Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Accessing the .toObject() of a subdocument inside of a method raises a typing error #14573

Closed
2 tasks done
viganll opened this issue May 6, 2024 · 3 comments
Closed
2 tasks done
Labels
typescript Types or Types-test related issue / Pull Request
Milestone

Comments

@viganll
Copy link

viganll commented May 6, 2024

Prerequisites

  • I have written a descriptive issue title
  • I have searched existing issues to ensure the bug has not already been reported

Mongoose version

8.3.3

Node.js version

18.17.1

MongoDB server version

5

Typescript version (if applicable)

4.8

Description

I have followed the guide to create types for sub-documents in mongoose. While, the approach works in normal code flow, when i try to access the subdocument properties such as .toObject or .ownerDocument inside a method it raises a typescript error.

Steps to Reproduce

import mongoose, { Types } from "mongoose";

// Subdocument definition
interface Names {
  _id: Types.ObjectId;
  firstName: string;
}

// Document definition
interface User {
  names: Names;
}

// Define property overrides for hydrated documents
type THydratedUserDocument = {
  names?: mongoose.Types.Subdocument<Names>;
};

type UserMethods = {
  getName(): string;
};

type UserModelType = mongoose.Model<User, {}, UserMethods, {}, THydratedUserDocument>;

const userSchema = new mongoose.Schema<User, UserModelType, UserMethods>(
  {
    names: new mongoose.Schema<Names>({ firstName: String }),
  },
  {
    methods: {
      getName() {
        // `this.names` is a subdocument, so we can call `toObject` on it
        // -> However, there is a typing error "Property 'toObject' does not exist on type 'Names'."
        return this.names.toObject();
      },
    },
  }
);
const UserModel = mongoose.model<User, UserModelType>("User", userSchema);

async function run() {
  await mongoose.connect("mongodb://localhost:27017");
  await mongoose.connection.dropDatabase();

  const doc = new UserModel({ names: { _id: "0".repeat(24), firstName: "foo" } });
  doc.names?.ownerDocument(); // Works, `names` is a subdocument!
}

run();

Expected Behavior

I would expect that there is no typing error when i try to access the subdocument props such as .toObject inside of a method.

@vkarpov15
Copy link
Collaborator

You also need to set THydratedDocumentType generic on Schema constructor to ensure you get the correct this type in methods and middleware. See the following script for a complete example.

import mongoose, { Types } from "mongoose";

// Subdocument definition
interface Names {
  _id: Types.ObjectId;
  firstName: string;
}

// Document definition
interface User {
  names: Names;
}

// Define property overrides for hydrated documents
type THydratedUserDocument = {
  names?: mongoose.HydratedSingleSubdocument<Names>;
};

type UserMethods = { 
  getName(): Names | undefined;
};

type UserModelType = mongoose.Model<User, {}, UserMethods, {}, THydratedUserDocument>;

const userSchema = new mongoose.Schema<
  User,
  UserModelType,
  UserMethods,
  {},
  {},
  {},
  mongoose.DefaultSchemaOptions,
  User,
  THydratedUserDocument
>(
  {
    names: new mongoose.Schema<Names>({ firstName: String }),
  },
  {
    methods: {
      getName() {
        const str: string | undefined = this.names?.firstName;
        return this.names?.toObject(); // Works, `this.names` is a subdoc
      },
    },
  }
);
const UserModel = mongoose.model<User, UserModelType>("User", userSchema);

async function run() {
  await mongoose.connect("mongodb://localhost:27017");
  await mongoose.connection.dropDatabase();

  const doc = new UserModel({ names: { _id: "0".repeat(24), firstName: "foo" } });
  const str: string | undefined = doc.names?.firstName;
  doc.names?.ownerDocument(); // Works, `names` is a subdocument!
}

run();

@viganll
Copy link
Author

viganll commented May 7, 2024

@vkarpov15 Thank you for looking into it.

While your example works, I still have two questions:

  1. What is the role of HydratedSingleSubdocument?
  2. In your example the main doc is not properly typed as now it is typed as THydratedUserDocument . e.g doc.getName() raises an error. Although there might be workarounds, it seems to add a layer of complexity. What would be the recommended way to handle it?

Would this be the right way?

type UserHydratedDocument = mongoose.HydratedDocument<User, UserMethods>;

// Define property overrides for hydrated documents
interface THydratedUserDocument extends Omit<UserHydratedDocument, "names"> {
  names?: mongoose.HydratedSingleSubdocument<Names>;
}

@viganll
Copy link
Author

viganll commented May 8, 2024

In addition, even after the adjustment you suggested, this refers to the lean User instead of hydrated version inside the required function e.g

  age: {
      type: Number,
      required: function () {

       this.names?.toObject() // typescript raises an error
        return true
      },
 },

vkarpov15 added a commit that referenced this issue May 9, 2024
docs(typescript): clarify that setting THydratedDocumentType on schemas is necessary for correct method context
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
typescript Types or Types-test related issue / Pull Request
Projects
None yet
Development

No branches or pull requests

2 participants