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

Type 'typeof Player' is not assignable to type 'typeof Model' #835

Closed
robpearmain opened this issue Sep 14, 2020 · 21 comments · Fixed by #900
Closed

Type 'typeof Player' is not assignable to type 'typeof Model' #835

robpearmain opened this issue Sep 14, 2020 · 21 comments · Fixed by #900

Comments

@robpearmain
Copy link

Versions

  • sequelize: sequelize@6.3.5
  • sequelize-typescript: sequelize-typescript@1.1.0
  • typescrip: typescript@4.0.2

I'm submitting a ...

[X] bug report
[ ] feature request

Actual behavior:
I have added 2 class files, Player.ts and Team.ts as per the example. In each of the references:

  @HasMany(() => Player)

and

  @ForeignKey(() => Team)
  @Column
  teamId: number;
 
  @BelongsTo(() => Team)
  team: Team;

The Team and the Player following the => on the attribute is underlines in red in VS code with the error:

Type 'typeof Team' is not assignable to type 'typeof Model'.
  Construct signature return types 'Team' and 'Model<T, T2>' are incompatible.
    The types of '_attributes' are incompatible between these types.
      Type 'Team' is not assignable to type 'T'.
        'T' could be instantiated with an arbitrary type which could be unrelated to 'Team'.ts(2322)
model-class-getter.d.ts(2, 40): The expected type comes from the return type of this signature.

Expected behavior:
No Errors

Steps to reproduce:
Added Player.ts with the sample code provided in the readme.md, and Team.ts in the same way

Related code:
Player.ts

import { Table, Model, Column, ForeignKey, BelongsTo } from 'sequelize-typescript'

@Table
export class Player extends Model<Player> {
 
  @Column
  name: string;
 
  @Column
  num: number;
 
  @ForeignKey(() => Team)
  @Column
  teamId: number;
 
  @BelongsTo(() => Team)
  team: Team;
}

Team.ts

import { Table, Model, Column, ForeignKey, HasMany } from 'sequelize-typescript'
import { Player } from './Player';


@Table
export class Team extends Model<Team> {
 
  @Column
  name: string;
 
  @HasMany(() => Player)
  players: Player[];
}
@abstractmatter
Copy link

abstractmatter commented Sep 16, 2020

It's related to sequelize@6.3.5. I have the same issue with both sequelize-typescript @1.1.0 & @2.0.0-beta.0

If I downgrade to sequelize@5.22.0 the issue is gone.

It must be a change in the sequelize Model type which is not compatible with the decorators typing.

Already reported in: #828, #826, #821 #813

Looks like using sequelize@6.1.1 solve the issue for now, which is the last version of sequelize with Model type compatible with sequelize-typescript.

@Austine105
Copy link

Austine105 commented Oct 7, 2020

It's related to sequelize@6.3.5. I have the same issue with both sequelize-typescript @1.1.0 & @2.0.0-beta.0

If I downgrade to sequelize@5.22.0 the issue is gone.

It must be a change in the sequelize Model type which is not compatible with the decorators typing.

Already reported in: #828, #826, #821 #813

Looks like using sequelize@6.1.1 solve the issue for now, which is the last version of sequelize with Model type compatible with sequelize-typescript.

using sequelize@6.1.1 actually solves the issue, but creates another issue. It messes with the dependency injection in when using NestJS, the error is given as:

Type '(attributes: ModelAttributes<Model<any, any>>, options: InitOptions<Model<any, any>>) => void' is not assignable to type '<M extends Model<any, any> = Model<any, any>>(this: ModelCtor<M>, attributes: ModelAttributes<M>, options: InitOptions<M>) => Model<...>'.ts(2684)

@eole1712
Copy link

Hello @Austine105, did you found any solution for this issue ? I have the same problem with nest.js

@Austine105
Copy link

Yes I did.

I used the factory pattern to configure sequelize as a provider and inject them in each module.

// database.providers.ts
export const databaseProviders = [
    {
        provide: SEQUELIZE, //  'SEQUELIZE'
        useFactory: async () => {
          const sequelize = new Sequelize(configService.getSequelizeConfig());
            sequelize.addModels([UserModel, OtherModels..]);
            await sequelize.sync();
            return sequelize;
        },
    },
];
// database.module.ts
@Module({
    providers: [...databaseProviders],
    exports: [...databaseProviders],
})
export class DatabaseModule { }

import DatabaseModule in your app.module file.
In a module, e.g UserModule, create the file below:

// user.provider.ts
export const UserProvider = [
  {
    provide: USER_REPOSITORY,  // 'USER_REPOSITORY'
    useValue: UserModel,
  }
];

Add the provider to UserModule

// user.module.ts
...imports...
providers: [UserService, ...UserProvider],  // the spread operator in UserProvider is intentional
...exports...

Finally in UserService constructor

// user.service.ts
constructor(
    @Inject(USER_REPOSITORY) private readonly userRepo: typeof UserModel
  ) { }


// in your methods, you can now use the userRepo, e.g this.userRepo.findAndCountAll(query...)

@hypertrends
Copy link

hypertrends commented Nov 11, 2020

Hello, similar to the approach outlined above, has anyone been able to build something with private declarations and a constructor model?

    import {Column, DataType, Model, Table} from 'sequelize-typescript';

@Table({
  timestamps: false,
  freezeTableName: true,
  tableName: 'author'
})
export class Author extends Model<Author> {
  
  @Column({
    type: DataType.NUMBER,
    allowNull: false,
    field: 'id',
    primaryKey: true,
  })
  private _id: Number;
  
  @Column({
    type: DataType.STRING,
    allowNull: false,
    field: 'name',
  })
  private _name: string;
  
  @Column({
    type: DataType.STRING,
    allowNull: false,
    field: 'code',
  })
  private _code: string;

  @Column({
    type: DataType.STRING,
    allowNull: false,
    field: 'created_at',
  })
  private _createdAt: Date;

  constructor(id: Number, name: string, code: string) {
    super();
    this._id = id;
    this._name = name;
    this._code = code;
    this._createdAt = new Date();
  }

  public updateAuthor(name: string, code: string) {
    this._name = name;
    this._code = code;
  }

  get id(): Number {
    return this._id;
  }

  get name(): string {
    return this._name;
  }

  get code(): string {
    return this._code;
  }

}

I would love to build a rich DDD style model, but I keep getting this error:

The 'this' context of type 'typeof Author' is not assignable to method's 'this' of type '(new () => Author) & typeof Model'.
Type 'typeof Author' is not assignable to type 'new () => Author'.

Would love to see if someone has tried similar approaches.

@cojack
Copy link

cojack commented Nov 24, 2020

Same here, @RobinBuschmann any idea what's going on?

@codemasternode
Copy link

I have the same isssue with sequelize@6.3.5 and sequelize-typescript@1.1.0

@jeanbispo
Copy link

I have the same problem with

"sequelize": "^6.3.5",
"sequelize-typescript": "^2.0.0-beta.0"

and

"sequelize": "^6.1.1",
"sequelize-typescript": "^2.0.0-beta.0"

resolved with downgrade to

"sequelize": "^5.22.0",
"sequelize-typescript": "^1.1.0"

@intellix
Copy link
Contributor

intellix commented Dec 13, 2020

I've created a simple reproduction in TypeScript Playground (Here). I'm not that good at TypeScript but when it's in it's simplest form it's easier to play around with it until the error disappears and we can PR/patch it. After playing with it a little I asked in https://gitter.im/Microsoft/TypeScript and a guy named webstrand helped me understand what was wrong:

The issue is actually in your definition of Model.
The TCreationAttributes extends {} = TModelAttributes causes problems because extends {} doesn't require a name: string property

Or, in other words: You cannot safely cast typeof Project into typeof Model because you construct Project like new Project({ name: "foo" /* this property is required */ })
Whereas, if you cast it to typeof Model, you would be able to unsafely call new (Project as typeof Model)({ }), which is an error.

You can fix by threading more generic information through. playground example
{ prototype: Model<{}, {}>; new (v?: {}, options?: any) => Model<{}, {}> } is roughly equivalent to typeof Model
I don't need prototype, so I just specify the constructor type in ModelClassGetter
Also, beware that typescript is structurally typed.

interface Foo { name: string } is exactly the same, and indistinguishable from, this interface Bar { name: string }
which is why I added attr to OriginModel, otherwise OriginModel<{ name: string }> is the same as OriginModel<{ id: 5000 }> as far as TypeScript is concerned
It doesn't care about or compare generic arguments unless they're used in some property or method defined on the class.

Here's the Playground example that fixes it, which also seems to work in other places like Includable which also has the same problem. It's a bit verbose but maybe it's the necessary evil to achieve what we want.

TL;DR:

export type TypeOfModel<TCreationAttributes, TModelAttributes> = new (values?: TCreationAttributes, options?: any) => Model<TModelAttributes, TCreationAttributes>;

export type Includeable<TCreationAttributes, TModelAttributes> = TypeOfModel<TModelAttributes, TCreationAttributes> | Association | IncludeOptions | { all: true, nested?: true } | string;

export declare type ModelClassGetter<TCreationAttributes, TModelAttributes> = (returns?: void) => TypeOfModel<TCreationAttributes, TModelAttributes>;

export declare function HasOne<TCreationAttributes, TModelAttributes>(associatedClassGetter: ModelClassGetter<TCreationAttributes, TModelAttributes>, foreignKey?: string): Function;
export declare function HasOne<TCreationAttributes, TModelAttributes>(associatedClassGetter: ModelClassGetter<TCreationAttributes, TModelAttributes>, options?: HasOneOptions): Function;

etc

@chorfa007
Copy link

I have the same problem with

"sequelize": "^6.3.5",
"sequelize-typescript": "^2.0.0-beta.0"

and

"sequelize": "^6.1.1",
"sequelize-typescript": "^2.0.0-beta.0"

resolved with downgrade to

"sequelize": "^5.22.0",
"sequelize-typescript": "^1.1.0"

Thanks your proposal can solve the issue

@KapitanOczywisty
Copy link
Contributor

You can use sequelize^6.3.5 + sequelize-typescript^2.0.0-beta.1, you only need to remove argument from Model<...>:

-export class Team extends Model<Team> {
+export class Team extends Model {

Thanks to @lukashroch for updated docs: https://github.com/lukashroch/sequelize-typescript/tree/sequelize6-docs#v6-model-definition-less-strict

@techird
Copy link

techird commented Jan 28, 2021

Hi guys, I provide a workaround to allow strict model attribute type-checks. Works for:

  • "sequelize": "^6.3.5",
  • "sequelize-typescript": "^2.0.0",
import {
  AutoIncrement,
  Column,
  HasMany,
  Model,
  ModelCtor,
  PrimaryKey,
  Sequelize,
  Table,
} from 'sequelize-typescript';

type ModelAttributes<T> = Omit<T, keyof Model>;
type CreationAttributes<T> = {
  [key in keyof ModelAttributes<T>]?: ModelAttributes<T>[key];
};

// 1. Created a strict typed model class
export class TypedModel<T> extends Model<ModelAttributes<T>, CreationAttributes<T>> {
  // provides a less strict type Model
  static get Model(): ModelCtor {
    return this as any;
  }
}

// 2. Extends TypedModel instead of Model
@Table
class User extends TypedModel<User> {
  @AutoIncrement
  @PrimaryKey
  @Column
  userId: number;

  @Column
  userName: string;
}

@Table
class Team extends TypedModel<Team> {
  @AutoIncrement
  @PrimaryKey
  @Column
  teamId: number;

  // 3. Use less strict type Model to make type check successfully
  @HasMany(() => User.Model)
  members: User[];
}

const sequelize = new Sequelize({
  // ... other initialize options

  // 4. Just use less strict type Model wherever you needs
  models: [User.Model, Team.Model],
});

User.findAll({
  // 5. now you got a type-safe where!
  where: {
    userName: 'techirdliu',
  },
});

@AndreasDvrs
Copy link

@KapitanOczywisty solution also works with:

  • "sequelize": "^6.5.0",
    
  • "sequelize-typescript": "^2.1.0"
    

So i think we can work free with the latest versions

@intellix
Copy link
Contributor

intellix commented Feb 21, 2021

I'm not sure if the above fixes the problem with Includeables:

PaymentCard.findOne({
  transaction,
  include: [BillingAddress],
  where: { active: true },
}),

Gives:

No overload matches this call.
  Overload 1 of 2, '(this: ModelStatic<PaymentCard>, options: NonNullFindOptions<PaymentCardAttrs>): Promise<PaymentCard>', gave the following error.
    Type 'typeof BillingAddress' is not assignable to type 'Includeable'.
      Type 'typeof BillingAddress' is not assignable to type 'typeof Model'.
        Types of construct signatures are incompatible.
          Type 'new (values?: BillingAddressCreateAttrs, options?: BuildOptions) => BillingAddress' is not assignable to type 'new <TModelAttributes extends {} = any, TCreationAttributes extends {} = TModelAttributes>(values?: TCreationAttributes, options?: BuildOptions) => Model<TModelAttributes, TCreationAttributes>'.
            Types of parameters 'values' and 'values' are incompatible.
              Type 'TCreationAttributes' is not assignable to type 'BillingAddressCreateAttrs'.
                Type '{}' is missing the following properties from type 'BillingAddressCreateAttrs': paymentCardId, name, lastName, address1, and 6 more.
  Overload 2 of 2, '(this: ModelStatic<PaymentCard>, options?: FindOptions<PaymentCardAttrs>): Promise<PaymentCard>', gave the following error.
    Type 'typeof BillingAddress' is not assignable to type 'Includeable'.
      Type 'typeof BillingAddress' is not assignable to type 'typeof Model'.ts(2769)

And I think it's the initial problem I mentioned in my original PR. That Sequelize has it's own Model types that we'd need to override somehow.

It works if you remove the Attrs and CreateAttr types:

@Table
export class PaymentCard extends Model {
  @HasOne(() => BillingAddress)
  public billingAddress: BillingAddress;
}

@Table
export class BillingAddress extends Model {...}

@theoludwig
Copy link
Contributor

There is a PR in the sequelize repo to fix this :

@therougeb
Copy link

Hello, how ab out this issue ?

Because i'm facing exactly the same.

I have a generic argument to a function that takes a model constructor like 'Runner' but it can't build because typeof Runner is n ot assignable to ModelCtor

src/controllers/runner.controller.ts(6,8): error TS2322: Type 'typeof Runner' is not assignable to type 'ModelCtor<Model<any, any>>'.
  Type 'typeof Runner' is not assignable to type 'typeof Model'.
    Types of construct signatures are incompatible.
      Type 'new (values?: RunnerCreateAttributes, options?: BuildOptions) => Runner' is not assignable to type 'abstract new <TModelAttributes extends { [property: string]: any; } = any, TCreationAttributes extends { [property: string]: any; } = TModelAttributes>(values?: TCreationAttributes, options?: BuildOptions) => Model<TModelAttributes, TCreationAttributes>'.
        Types of parameters 'values' and 'values' are incompatible.
          Type 'TCreationAttributes' is not assignable to type 'RunnerCreateAttributes'.
            Type '{ [property: string]: any; }' is not assignable to type 'Optional<RunnerAttributes, "id">'.
              Type '{ [property: string]: any; }' is not assignable to type 'Omit<RunnerAttributes, "id">'.
                Type 'TCreationAttributes' is not assignable to type 'Omit<RunnerAttributes, "id">'.
                  Type '{ [property: string]: any; }' is missing the following properties from type 'Omit<RunnerAttributes, "id">': email, avatar_url, firstname, lastname, and 14 more.

it's very curious because if this type doesn't help to assign a Model to a typed property how can we achieve this ?

is this a bug or a misunderstanding about how to use sequelize types ?

here is my code:

export interface CrudOpts {
  Model?: ModelCtor<Model>;
  only?: string[];
  except?: string[];
  nestedRelations?: boolean;
  basepath?: string;
}

import { NextFunction, Request, Response, Router } from "express";
import { FindOptions, Model, ModelCtor } from "sequelize";
import { CrudOpts } from "./types";

export class Crud {
  Model: ModelCtor<Model>;
  only?: string[];
  except?: string[];
  constructor(opts: CrudOpts) {
    this.Model = opts.Model;
    this.only = opts.only;
    this.except = opts.except
  }
  createCrud(): Router {
    const router = Router();

    router.get(`/`, this.findAll.bind(this))
    router.post(`/`, this.create.bind(this))
    router.get(`/:id`, this.findById.bind(this))
    router.put(`/:id`)
    router.patch(`/:id`)
    router.delete(`/:id`)

    return router;
  }

  async findAll(req: Request, res: Response) {
    const { query: { limit, page } } = req;
    const query: FindOptions = {};

    if (limit) {
      const parsedLimit = Number(limit);
      query.limit = parsedLimit;
      query.offset = parsedLimit * Number(page) - 1
    }

    return res.status(200).json(await this.Model.findAll()).end();
  }

  async create(req: Request, res: Response, next: NextFunction) {
    const { body } = req;
    const newRecord = this.Model.build(body);

    try {
      await newRecord.validate();
      await newRecord.save();
      await newRecord.reload();

      return res.status(201).json(newRecord).end()
    } catch (err) {
      console.error(err);
      return next(err);
    }
  }

  async findById(req: Request, res: Response) {
    const record = await this.Model.findByPk(req.params.id);

    if (!record) {
      return res.status(404).json({
        statusCode: 404,
        code: 'ModelNotFound',
        message: `Current resource not found at ${req.method} - ${req.url}`
      })
    }
    return res.status(200).json()
  }
}

import { Application, Errback, NextFunction, Response, Router } from "express";
import { ValidationError } from "sequelize";
import * as controllers from '../controllers';
import { Crud } from "./crud";
import { CrudOpts, RouteDef } from './types'

export class Core {
  static createApplication(express: any): Application {
    for(const controller in controllers){
      const Controller = controllers[controller].prototype;
      const basepath = Reflect.getMetadata('basepath', Controller) || '';
      const routes: RouteDef[] = Reflect.getMetadata('routes', Controller) || [];
      const crud: CrudOpts = Reflect.getMetadata('crud', Controller);
      const router = Router();

      for(const config of routes){
        router[config.method](`${config.path}`, config.handler);
      }

      if(crud) express.use(`/${basepath}`, Core.crud(crud));

      express.use(`/${basepath}`, router)
    }

    express.use(function (err: any, req: Request, res: Response, next: NextFunction){
      console.info(err, req.url);

      if(err instanceof ValidationError){
        res.status(400).json({
          statusCode: 400,
          message: err.message,
          code: err.name,
          stack: err.stack
        })
      }

      next();
    })

    return express;
  }

  static crud(opts: CrudOpts): Router {
    const crud = new Crud(opts);

    return crud.createCrud();
  }
};

@crud({Model: Runner})
export class RunnerController {
 }

thanks for help

@Austine105
Copy link

Have you looked at the solution above

@KapitanOczywisty
Copy link
Contributor

@therougeb Provided code is missing crucial parts, but I'm guessing you are mixing ModelCtor with ModelType. If you could provide minimal example with such error it would be easier to reproduce.

@MaartenRimaux
Copy link

MaartenRimaux commented Aug 3, 2021

For people wondering in which release this issue was resolved in sequelize: v6.6.1

@RobMico
Copy link

RobMico commented Mar 2, 2023

I have been rewriting an old pure js code to ts and have got this error. The issue was that I had used the Model class from the sequelize package instead of the Model class from the sequelize-typescript package.

@levsim2016
Copy link

@RobMico you're saver. thank you!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging a pull request may close this issue.