Automattic / mongoose

MongoDB object modeling designed to work in an asynchronous environment.
https://mongoosejs.com
MIT License
26.97k stars 3.84k forks source link

InferSchemaType with instance methods #12013

Closed heilmela closed 2 years ago

heilmela commented 2 years ago

Prerequisites

Issue

So I wanna use the newly added InferSchemaType to remove boilerplate interfaces. However im not sure how would one add InstanceMethods, Virtuals, etc. to the Schema without defining the now inferred DocType. When leaving EnforceDocType and Model as defaults, the inferred document type is { [x: string]: any] }. Unfortunatly, typescript does not allow to skip generic parameters for now (https://github.com/microsoft/TypeScript/issues/10571)

// Schema
const schema = new Schema<
  any,
  mongoose.Model<any, any, any, any, any>,
  {
    test: () => void;
  }
>({
  name: { type: String, required: true },
  email: { type: String, required: true },
  avatar: String,
});

type User = InferSchemaType<typeof schema>;
// InferSchemaType will determine the type as follows:
// type User = {
//   [x: string]: any;
// };
iammola commented 2 years ago

Defining the instance methods, statics and query helpers directly in the Schema options will cause them to be inferred automatically.

Read the Documentation. https://mongoosejs.com/docs/guide.html#methods.

If you must, you can extract the inferred methods/statics/query helpers with the ObtainSchemaGeneric. That's how the InferSchemaType works as well

PeterPanen commented 2 years ago

Defining the instance methods, statics and query helpers directly in the Schema options will cause them to be inferred automatically.

Read the Documentation. https://mongoosejs.com/docs/guide.html#methods.

If you must, you can extract the inferred methods/statics/query helpers with the ObtainSchemaGeneric. That's how the InferSchemaType works as well

I can't seem to get this to work with my instance methods, but I'm probably doing something wrong. I made this simple schema:

const schema = new Schema(
  {
    name: { type: String, unique: true, required: true },
  },
  {
    methods: {
      someMethod() {
        return "test";
      },
    },
  }
);

type User = InferSchemaType<typeof schema>;

And the User type still only contains the name like shown here, no methods:

type User = {
    name: string;
}
PeterPanen commented 2 years ago

Wait i just realized that if i remove the InferSchemaType line in my example, and just let it infer automatically without me doing anything, it includes the instance methods :partying_face:

heilmela commented 2 years ago

Im still not sure what exactly im doing wrong, but in my case the inferred Document Type doesnt seem to include instance methods:

// Schema
const schema = new Schema(
  {
    name: { type: String, required: true },
    email: { type: String, required: true },
    avatar: String,
  },
  {
    methods: {
      test: (a: string) => a,
    },
  },
);

const UserModel = mongoose.model('User', schema);
UserModel.findOne()
  .exec()
  .then((x) => {
    //  (parameter) x: (mongoose.Document<unknown, any, {
    //     name: string;
    //     email: string;
    //     avatar?: string | undefined;
    // }> & {
    //     name: string;
    //     email: string;
    //     avatar?: string | undefined;
    // } & {
    //     _id: mongoose.Types.ObjectId;
    // } & {
    //     ...;
    // }) | null
    x.test('a');
    // Property 'test' does not exist on type
    //'(Document<unknown, any, { name: string; email: string; avatar?: string | undefined; }> & { name: string; email: string; avatar?: string | undefined; }
    //& { _id: ObjectId; } & { ...; })[]'.
  });
iammola commented 2 years ago

@heilmela

I'm not sure, because your commented-out code looks like it's an array, but you're querying the Model with findOne, so the result could also be null.

Check if the result is not null. If that's not the fix, try getting a single document from the result if it's an array with something likex[index].

heilmela commented 2 years ago

@iammola i updated the comment, it was a false leftover from a previous edit where i used find(). You are absolutely correct the union with null did produce the error. This works:


UserModel.findOne()
  .exec()
  .then((x) => {
    //  (parameter) x: (mongoose.Document<unknown, any, {
    //     name: string;
    //     email: string;
    //     avatar?: string | undefined;
    // }> & {
    //     name: string;
    //     email: string;
    //     avatar?: string | undefined;
    // } & {
    //     _id: mongoose.Types.ObjectId;
    // } & {
    //     ...;
    // }) | null
    if (x !== null) x.test('a');
  });