8

I want to extend a Sequelize Model class to add other instance methods but typescript keeps complaining that "Property 'prototype' does not exist on type 'Model'"

const MyModel = (sequelize: Sequelize.Sequelize, dataTypes: Sequelize.DataTypes) => {
  const User = sequelize.define<Instance, Attribute>(
    "users",
    {
      id: {
        type: dataTypes.INTEGER,
        primaryKey: true,
        autoIncrement: true,
      },
      email: {
        type: dataTypes.STRING
      },
      ...
    },
    {
      tableName: "users",
      ...
    },
  );

  User.prototype.verifyUser = function(password: string) {
    ...
  };

  return User;
};

I expect User.prototype.verifyUser to work but typescript complains. How to add to typings?

4 Answers 4

4

Following @Shadrech comment, I've an alternative (less hacky and abstract).

export interface UserAttributes {
   ...
}

export interface UserInstance extends Sequelize.Instance<UserAttributes>, UserAttributes {
}

interface UserModelInstanceMethods extends Sequelize.Model<UserInstance, UserAttributes> {

  // Came to this question looking for a better approach to this
  // You'll need root's definitions for invocation and prototype's for creation
  verifyPassword: (password: string) => Promise<boolean>;
  prototype: {
    verifyPassword: (password: string) => Promise<boolean>;
  };
}

const MyModel = (sequelize: Sequelize.Sequelize, dataTypes: Sequelize.DataTypes): UserModelInstanceMethods => {
  const User = sequelize.define<UserInstance, UserAttributes>(
      ...
  ) as UserModelInstanceMethods;

  User.prototype.verifyUser = function(password: string) {
    ...
  };

  return User;
}

Using your model:

sequelize.query("SELECT ...").then((user: UserInstance & UserModelInstanceMethods) => {
  user.verifyPassword(req.body.password) // <= from UserModelInstanceMethods
  user.getDataValue('name') // <= from UserInstance
})
Sign up to request clarification or add additional context in comments.

Comments

2

According to the main Sequelize TypeScript Doc, I think that the best way to implement it is using DataTypes.VIRTUAL and skip the property with TypeScript Omit utility on the model creation interface.

Important! Remember the Issue#11675!

A simple example:

import {
  Sequelize,
  Model,
  ModelDefined,
  DataTypes,
  Optional,
  // ...
} from 'sequelize';

interface ProjectAttributes {
  id: number;
  ownerId: number;
  name: string;
  readonly createdAt: Date;
  readonly updatedAt: Date;

  // #region Methods
  
  myMethod(name: string): Promise<void>; // <<<===

  // #endregion
}

interface ProjectCreationAttributes extends Omit< // <<<===
  Optional<
    ProjectAttributes,
    | 'id'
    | 'createdAt'
  >,
  'myMethod' // <<<===
> {}

class Project extends Model<ProjectAttributes, ProjectCreationAttributes>
  implements ProjectAttributes {
  public id: ProjectAttributes['id'];
  public ownerId: ProjectAttributes['ownerId'];
  public name: ProjectAttributes['name'];
  public readonly createdAt: ProjectAttributes['createdAt'];
  public readonly updatedAt: ProjectAttributes['updatedAt'];

  public readonly myMethod: ProjectAttributes['myMethod'] // <<<===

   /**
   * Initialization to fix Sequelize Issue #11675.
   *
   * @see https://stackoverflow.com/questions/66515762/configuring-babel-typescript-for-sequelize-orm-causes-undefined-properties
   * @see https://github.com/sequelize/sequelize/issues/11675
   * @ref #SEQUELIZE-11675
   */
  constructor(values?: TCreationAttributes, options?: BuildOptions) {
    super(values, options);

    // All fields should be here!
    this.id = this.getDataValue('id');
    this.ownerId = this.getDataValue('ownerId');
    this.name = this.getDataValue('name');
    this.createdAt = this.getDataValue('createdAt');
    this.updatedAt = this.getDataValue('updatedAt');

    this.myMethod = async (name) => { // <<<===
      // Implementation example!
      await this.update({
        name,
      });
    };
  }

  // #region Methods

  public toString() {
    return `@${this.name} [${this.ownerId}] #${this.id}`;
  }

  // #endregion
}

Project.init(
  {
    id: {
      type: DataTypes.INTEGER.UNSIGNED,
      autoIncrement: true,
      primaryKey: true,
    },
    ownerId: {
      type: DataTypes.INTEGER.UNSIGNED,
      allowNull: false,
    },
    name: {
      type: new DataTypes.STRING(128),
      allowNull: false,
    },


    myMethod: { // <<<===
      type: DataTypes.VIRTUAL(DataTypes.ABSTRACT),
    }
  },
  {
    sequelize,
    tableName: "projects",
  }
);

1 Comment

Thanks for this example, I have ben running into the issue #11675 but did not yet know how to solve. One nit: Your constructor uses a type 'TCreationAttributes' and I believe you meant for it to be 'ProjectCreationAttributes'
1

One solution I've seen is where you force type after declaring the Model. So

interface UserModelInstanceMethods extends Sequelize.Model<Instance, Attributes> {
  prototype: {
    verifyPassword: (password: string) => Promise<boolean>;
  };
}

const MyModel = (sequelize: Sequelize.Sequelize, dataTypes: Sequelize.DataTypes) => {
  const User = sequelize.define<Instance, Attribute>(
    "users",
    {
      id: {
        type: dataTypes.INTEGER,
        primaryKey: true,
        autoIncrement: true,
      },
      email: {
        type: dataTypes.STRING
      },
      ...
    },
    {
      tableName: "users",
      ...
    },
  );

  User.prototype.verifyUser = function(password: string) {
    ...
  };

  return User;
} as Sequelize.Model<Instance, Attributes> & UserModelInstanceMethods;

Comments

0

Step 1:

Define a new type that will describe the definition of the model DefinedModel. In addition receive a generic T to get responses from database defined by an interface.

Step 2:

Create an instance of the model parsing the connection.define return to our DefinedModel.

// Step 0: Declarations
const connection: Sequelize = new Sequelize({...});
const modelName: string = '...';
const definition: ModelAttributes = {...};
const options: ModelOptions = {...};
interface MyInterface {...}; // Should describe table data

// Step 1
type DefinedModel<T> = typeof Model & {
  new(values?: object, options?: BuildOptions): T;
}

// Step 2
const model: DefinedModel<Model> = <DefinedModel<Model>>connection.define(modelName, definition, options);

// Step 2 with Interface definition
const iModel: DefinedModel<MyInterface & Model> = <DefinedModel<MyInterface & Model>> connection.define(modelName, definition, options);

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.