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

Typescript: Typed model lacking type inference within the definitions of static and instance methods #10358

Closed
ajwootto opened this issue Jun 14, 2021 · 6 comments
Labels
help This issue can likely be resolved in GitHub issues. No bug fixes, features, or docs necessary

Comments

@ajwootto
Copy link

Do you want to request a feature or report a bug?
bug?

What is the current behavior?
When constructing a model that uses Typescript and contains type definitions for static and instance methods, the types of those methods are not "inferred" when implementing the methods later on. A code example is below that might make things clearer.

If the current behavior is a bug, please provide the steps to reproduce.
See this script:

import { Schema, Model, createConnection } from 'mongoose'

interface ITestModel {
    name: string
}

interface InstanceMethods {
    iMethod1: (param1: string) => string
    iMethod2: (param1: string) => string
}

interface TestModel extends Model<ITestModel, {}, InstanceMethods> {
    sMethod1: (param1: string) => string
    sMethod2: (param1: string) => string
}

const ModelSchema = new Schema<ITestModel, TestModel>({
    name: String
})

// NOT a type error (incorrect)
ModelSchema.statics.sMethod1 = function() {
    // type error (correct, params are wrong)
    this.sMethod2()
}

// NOT a type error (incorrect)
ModelSchema.statics.sMethod2 = function() {

}

// NOT a type error (incorrect)
ModelSchema.methods.iMethod1 = function() {
    // type error (incorrect, this method exists)
    this.iMethod2("test")
}

// NOT a type error (incorrect)
ModelSchema.methods.iMethod2 = function() {

}

// create lazy connection object to connect later
const connection = createConnection()

export const TestModelCompiled = connection.model<ITestModel, TestModel>("testModel", ModelSchema)

const modelInstance = new TestModelCompiled()

// type error (correct, method exists but param omitted)
modelInstance.iMethod1()
// type error (correct, method exists but param omitted)
TestModelCompiled.sMethod1()

What I was hoping for by defining the types for these models is that the implementations for each instance and static method would infer their types from the definitions provided to the schema. I would also expect that the this context in both the instance and static methods would have knowledge of the other instance / static methods available on the model.

By this example:

  1. The instance and static methods themselves don't seem to be inferring their types from the type definitions above. This is also the case when using the schema.static('methodName, function() {}) style syntax. This allows me to implement the methods in a way that violates the type definitions
  2. The static methods seem to be aware of the other static methods defined on the model, so when I call this.sMethod2() with no arguments I (correctly) get a type error. This is not the case in instance methods, where this.iMethod2() tells me the method is not available.

Both instance and static methods have correct types when using them outside the model's methods themselves, as seen on the last two lines.

This may also be due to a misunderstanding on my part of how to define the model's types. I'm relatively new to Typescript so forgive me if I'm getting something wrong.

tsconfig.json

{
    "compilerOptions": {
        "allowJs": true,
        "checkJs": false,
        "esModuleInterop": true,
        "skipLibCheck": true,
        "noEmit": true,
        "resolveJsonModule": true
    },
    "include": [
        "server-src",
        "lib",
        "src",
        "__tests__"
    ],
    "exclude": [
        "node_modules/**/*",
        "../../node_modules/**/*"
    ]
}

What is the expected behavior?
See above, I was expecting the instance and static methods to correctly infer their types from the model's type definition, and for the instance method context to contain the other instance methods defined on the type.

What are the versions of Node.js, Mongoose and MongoDB you are using? Note that "latest" is not a version.
mongoose: 5.12.13
Node.js: 14.15.0
Typescript: 4.2.3

@IslandRhythms IslandRhythms added confirmed-bug We've confirmed this is a bug in Mongoose and will fix it. typescript Types or Types-test related issue / Pull Request labels Jun 15, 2021
@vkarpov15 vkarpov15 added this to the 5.12.15 milestone Jun 15, 2021
@vkarpov15
Copy link
Collaborator

Most of the issues can be resolved by using new Schema<ITestModel, Model<ITestModel, {}, InstanceMethods>> instead of new Schema<ITestModel, TestModel>, because Schema tries to infer InstanceMethods and it doesn't look like TypeScript can pull out generics from base classes using infer.

That just leaves the Property 'sMethod2' does not exist on type 'Model<ITestModel, {}, InstanceMethods>' error. We'll look into this and see if we can fix the issue - I'd hate to have to add yet another generic parameter to Schema or Model but that might be the only workaround.

@vkarpov15
Copy link
Collaborator

I can confirm the below script compiles successfully with 928ca4a. The issue is that you'll need to add a 4th generic param to the Schema constructor as shown below:

import { Schema, Model, createConnection } from 'mongoose'

interface ITestModel {
    name: string
}
interface InstanceMethods {
    iMethod1: (param1: string) => string
    iMethod2: (param1: string) => string
}
interface TestModel extends Model<ITestModel, {}, InstanceMethods> {
    sMethod1: (param1: string) => string
    sMethod2: (param1: string) => string
}
const ModelSchema = new Schema<ITestModel, TestModel, undefined, InstanceMethods>({ // <-- add `InstanceMethods` here
    name: String
})

ModelSchema.statics.sMethod1 = function() {
    this.sMethod2('test')
}
ModelSchema.statics.sMethod2 = function() {}

ModelSchema.methods.iMethod1 = function() {
    this.iMethod2("test")
}
ModelSchema.methods.iMethod2 = function() {
}

// create lazy connection object to connect later
const connection = createConnection()

export const TestModelCompiled = connection.model<ITestModel, TestModel>("testModel", ModelSchema)

const modelInstance = new TestModelCompiled()

modelInstance.iMethod1('test')

TestModelCompiled.sMethod1('test')

steve1337 added a commit to steve1337/mongoose that referenced this issue Nov 18, 2021
When using mongoose w/ typescript this comment explains how to create Schema and Model with instance methods:

Automattic#10358 (comment)

```
import { Schema, Model, createConnection } from 'mongoose'

interface ITestModel {
    name: string
}
interface InstanceMethods {
    iMethod1: (param1: string) => string
    iMethod2: (param1: string) => string
}
interface TestModel extends Model<ITestModel, {}, InstanceMethods> {
    sMethod1: (param1: string) => string
    sMethod2: (param1: string) => string
}
const ModelSchema = new Schema<ITestModel, TestModel, undefined, InstanceMethods>({ // <-- add `InstanceMethods` here
    name: String
})

ModelSchema.statics.sMethod1 = function() {
    this.sMethod2('test')
}
ModelSchema.statics.sMethod2 = function() {}

ModelSchema.methods.iMethod1 = function() {
    this.iMethod2("test")
}
ModelSchema.methods.iMethod2 = function() {
}

// create lazy connection object to connect later
const connection = createConnection()

export const TestModelCompiled = connection.model<ITestModel, TestModel>("testModel", ModelSchema)

const modelInstance = new TestModelCompiled()

modelInstance.iMethod1('test')

TestModelCompiled.sMethod1('test')
```

However due to the change made in this commit: Automattic@fefebb3

The above mentioned code won't compile. The error occurs on this line:
`export const TestModelCompiled = connection.model<ITestModel, TestModel>("testModel", ModelSchema)`

Output in VSCode (v1.62.2):
```
No overload matches this call.
  Overload 1 of 3, '(name: string, schema?: Schema<ITestModel, TestModel, {}, {}>, collection?: string, skipInit?: boolean): TestModel', gave the following error.
    Argument of type 'Schema<ITestModel, TestModel, undefined, InstanceMethods>' is not assignable to parameter of type 'Schema<ITestModel, TestModel, {}, {}>'.
      Type '{}' is missing the following properties from type 'InstanceMethods': iMethod1, iMethod2
  Overload 2 of 3, '(name: string, schema?: Schema<any, Model<any, any, any>, undefined, {}>, collection?: string, skipInit?: boolean): TestModel', gave the following error.
    Argument of type 'Schema<ITestModel, TestModel, undefined, InstanceMethods>' is not assignable to parameter of type 'Schema<any, Model<any, any, any>, undefined, {}>'.
      Type '{}' is not assignable to type 'InstanceMethods'.ts(2769)
const ModelSchema: Schema<ITestModel, TestModel, undefined, InstanceMethods>
```
@shahar1
Copy link

shahar1 commented Feb 19, 2022

@vkarpov15 Using this pattern, how can I refer to document properties within an instance method? For example, the following yields a TypeScript error:

interface ITestModel {
    name: string
}
interface InstanceMethods {
    iMethod1: () => string
}
interface TestModel extends Model<ITestModel, {}, InstanceMethods> {

}
const ModelSchema = new Schema<ITestModel, TestModel, InstanceMethods>({
    name: String
})


ModelSchema.methods.iMethod1 = function() {
    return this.name // <-- yields "TS2339: Property 'name' does not exist on type '{ iMethod1: (param1: string) => string; }'."
}

@hungdao-testing
Copy link

hungdao-testing commented Feb 22, 2022

Searching few tutorials, to help TS (or exactly is the Code Editor) find and suggest the instance methods, the Document interface should declare the method. Here are what I wanna say:

In the model file userModel.ts (declare the method comparePassword role as instanceMethod in the Document interface)

interface IUserDocument extends Document {
    name: string;
    email: string;
    photo?: string;
    password: string;
    passwordConfirm: string | undefined;
}

export interface IUser extends IUserDocument {
    comparePassword: (password1: string, password2: string) => Promise<boolean>
}

interface IUserModel extends Model<IUserDocument, {}> { }

const userSchema = new Schema<IUser, IUserModel>({
  ....
})

userSchema.methods.correctPassword = async function (candidatePassword: string, userPassword: string) {

    return await bcrypt.compare(candidatePassword, userPassword);
}

export const UserModel = mongoose.model<IUser>('User', userSchema);

In the controller file userController.ts, the VScode understands the definition of the instance methods when typing

import {UserModel} from './userModel.ts'
async (req: Request, res: Response, next: NextFunction) => {
        const { email, password } = req.body;
        const user = await UserModel.findOne({ email }).select('+password');

        
        const isPasswordCorrect = await user.correctPassword(password, user.password)
    }

The questions here this is the correct and effective ways to do ? Need you guys to suggest and share your thoughts

Ref: https://stackoverflow.com/questions/42448372/typescript-mongoose-static-model-method-property-does-not-exist-on-type/45675548#45675548

@vkarpov15 vkarpov15 reopened this Mar 2, 2022
@vkarpov15 vkarpov15 added help This issue can likely be resolved in GitHub issues. No bug fixes, features, or docs necessary and removed confirmed-bug We've confirmed this is a bug in Mongoose and will fix it. typescript Types or Types-test related issue / Pull Request labels Mar 2, 2022
@vkarpov15 vkarpov15 modified the milestones: 5.12.15, 6.2.7 Mar 2, 2022
@vkarpov15 vkarpov15 modified the milestones: 6.2.8, 6.2.11 Mar 11, 2022
@vkarpov15
Copy link
Collaborator

@hungdao-testing it looks like the code you pasted gets autocompleted fine in VSCode with Mongoose 6.2.x:

image

@vkarpov15 vkarpov15 removed this from the 6.2.11 milestone Apr 10, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
help This issue can likely be resolved in GitHub issues. No bug fixes, features, or docs necessary
Projects
None yet
Development

No branches or pull requests

5 participants