node.jstypescriptmongoosetsc

TypeScript can't compile Mongoose query helper


I'm working on a TypeScript project that uses Mongoose and is compiled with tsc. When I try to use a query helper with .find():

import { InferRawDocType, Schema, model } from "mongoose";

export const mySchemaDefinition = {
  key: "string",
  value: "string"
} as const;
export type RawSchema = InferRawDocType<typeof mySchemaDefinition>;
export const mySchema = new Schema(mySchemaDefinition, {
  query: {
    byKey: function (key: string) {
      return this.where({ key: key });
    }
  }
});
export const MyModel = model("MyModel", mySchema);

MyModel.find({ key: "123" }).exec(); // OK
MyModel.findOne().byKey("123").exec(); // OK
MyModel.find().byKey("123").exec(); // Error

TypeScript throws an error during transpilation:

The 'this' context of type 'QueryWithHelpers<(Document<unknown, { byKey: <T extends Query<unknown, Document<unknown, {}, FlatRecord<{ readonly key: "string"; readonly value: "string"; }>> & FlatRecord<{ readonly key: "string"; readonly value: "string"; }> & { _id: ObjectId; } & { __v: number; }, {}, FlatRecord<{ readonly key: "string"; readonly value: "string"; }>, "find", Record<string, never>>>(this: T, key: string) => T; }, { readonly key: "string"; readonly value: "string"; }> & { readonly key: "string"; readonly value: "string"; 
} & { _id: ObjectId; } & { __v: number; })[], Document<unknown, { byKey: <T extends Query<unknown, Document<unknown, {}, FlatRecord<{ readonly key: "string"; readonly value: "string"; }>> & FlatRecord<{ readonly key: "string"; readonly value: "string"; }> & { _id: ObjectId; } & { __v: number; }, {}, FlatRecord<{ readonly key: "string"; readonly value: "string"; }>, "find", Record<string, never>>>(this: T, key: string) => T; }, { readonly key: "string"; readonly value: "string"; }> & { readonly key: "string"; readonly value: "string"; } & { _id: ObjectId; } & { __v: number; }, { byKey: <T extends Query<unknown, Document<unknown, {}, FlatRecord<{ readonly key: "string"; readonly value: "string"; }>> & FlatRecord<{ readonly key: "string"; readonly value: "string"; }> & { _id: ObjectId; } & { __v: number; }, {}, FlatRecord<{ readonly key: "string"; readonly value: "string"; }>, "find", Record<string, never>>>(this: T, key: string) => T; }, { readonly key: "string"; readonly value: "string"; }, "find", {}>' is not assignable to method's 'this' of type 'Query<unknown, Document<unknown, {}, FlatRecord<{ readonly key: "string"; readonly value: "string"; 
}>> & FlatRecord<{ readonly key: "string"; readonly value: "string"; }> & { _id: ObjectId; } & { __v: number; }, {}, FlatRecord<{ readonly key: "string"; readonly value: "string"; }>, "find", Record<string, never>>'.
  The types returned by 'lean(...).exec()' are incompatible between these types.
    Type 'Promise<({ readonly key: "string"; readonly value: "string"; } & { _id: ObjectId; } & { __v: number; })[]>' is not assignable to type 'Promise<{ readonly key: "string"; readonly value: "string"; } & { _id: ObjectId; } & { __v: number; }>'.
      Type '({ readonly key: "string"; readonly value: "string"; } & { _id: ObjectId; } & { __v: number; })[]' is not assignable to type '{ readonly key: "string"; readonly value: "string"; } & 
{ _id: ObjectId; } & { __v: number; }'.
        Type '({ readonly key: "string"; readonly value: "string"; } & { _id: ObjectId; } & { __v: number; })[]' is missing the following properties from type '{ readonly key: "string"; readonly value: "string"; }': key, value

But it only happens with .find, .findOne works fine.
The this context seems to be the right type.
Is this a problem with tsc configuration or is there a Mongoose type error I can fix?

Build command:

tsc index.ts --esModuleInterop --outDir ./build --noErrorTruncation

tsconfig.json (mostly targets the React app that is in the same project):

{
  "compilerOptions": {
    "target": "es5",
    "lib": ["dom", "dom.iterable", "esnext"],
    "allowJs": true,
    "skipLibCheck": true,
    "esModuleInterop": true,
    "allowSyntheticDefaultImports": true,
    "strict": true,
    "forceConsistentCasingInFileNames": true,
    "noFallthroughCasesInSwitch": true,
    "module": "esnext",
    "moduleResolution": "node",
    "resolveJsonModule": true,
    "isolatedModules": true,
    "noEmit": true,
    "jsx": "react-jsx"
  },
  "include": ["src"]
}

Solution

  • The error occurs because the byKey query helper is not properly typed to handle both find() and findOne() operations. The current implementation assumes a single document return type, but find() returns an array. I hope it will fix.

    export const mySchemaDefinition = {
      key: "string",
      value: "string"
    } as const;
    export const mySchema = new Schema(mySchemaDefinition, {
    query: {
    byKey: function<T extends Query<any, any>>(this: T, key: string) {
     return this.where({ key: key });
        }
      }
    });
    

    This change makes the byKey query helper generic enough to work with both find() and findOne() operations.

    To avoid using any in Typescript code you can define a specific type for your schema and use it in the Query type. Here's how you can do it:

    type MySchemaType = {
      key: string;
       value: string;
    };
    
    export const mySchemaDefinition = {
     key: "string",
    value: "string"
    } as const;
    export const mySchema = new Schema(mySchemaDefinition, {
     query: {
     byKey: function<T extends Query<MySchemaType, typeof 
     mySchemaDefinition>>(this: T, key: string) {
     return this.where({ key: key });
      }
    }
    });