Typescript: return type of the child class from method

Viewed 304

I have a Model class and 2 child classes who extends from Model, like this:

Base Model module:

/**
 * 
 *  Provider: Model
 *  @module providers/core/utils/model
 *  
 *  @description provides an easy to handle sequelize model
 * 
 */
import { Model as SequelizeModel, InitOptions, ModelAttributes } from 'sequelize'
import sequelize from '@core/libs/sequelize'


export default
    /**
     * 
     *  @class Model @extends SequelizeModel
     *  @description provides a more simply layer to use models
     * 
     */
    class Model extends SequelizeModel {

        /**
         *  @default options
         */
        public static options: InitOptions = {
            sequelize,
            timestamps: true
        }

        /**
         *  @custom options
         */
        public static $options: object = {}

        /**
         *  @model definition
         */
        public static attributes: ModelAttributes = {}


        /**
         *  @init the model
         */
        public static init (): SequelizeModel { // instead of return SequelizeModel I need to return the class who calls itself... (this)

            // here just call init and not return yet
            return super.init.call( this, this.attributes, {
                ...this.options,
                ...this.$options
            })

            // and add here return this
        }

    }
;

Classes that extends from Model:

/**
 *
 *  Model: User
 *  @module app/models/user
 * 
 *  @description basic user model
 * 
 */
import { Model } from '@bananasplit-js' // this is the Model above
import { DataTypes, ModelAttributes } from 'sequelize'


class User extends Model {

    /**
     *  @fields
     */
    public id!: number
    public name!: string
    public lastname!: string
    public email!: string
    public password!: string


    /**
     *  @model
     */
    public static attributes: ModelAttributes = {

        id: {
            type: DataTypes.INTEGER.UNSIGNED,
            autoIncrement: true,
            
            primaryKey: true
        },

        name: {
            type: DataTypes.STRING,
            allowNull: false
        },

        lastname: {
            type: DataTypes.STRING,
            allowNull: false
        },

        email: {
            type: DataTypes.STRING,
            allowNull: false
        },

        password: {
            type: DataTypes.STRING,
            allowNull: false
        }
        
    }


    /**
     *  @options
     */
    public static $options: object = {
        timestamps: true
    }

}


export default User.init() // need to return User class (for static methods usage)
// the same with product model
class Product extends Model {
  ... // same as User model but for products
}

Product.init() // need to return Product class

I want to avoid two lines:

User.init()
export default User

And make it one:

export default User.init()

So, I need that when calling User.init() or Product.init() or AnyClass.init() from classes that extends from Model, the returned type be the type of the class who calls init method: User|Product|AnyClass

Is important to mention that my Model class is extending from the Sequelize Model class as well, which uses static methods.

So in my controller I use User model like this:

const users = User.findAll() // a list of all users

You can get the workspace running from gitpod:

https://gitpod.io/#snapshot/0a676aff-fdf2-4585-b349-53516d0c677e

Run the server: yarn dev (url path: /test-seeder) this is optional

Run the test (should pass if all is okay): yarn test setup

I need to do those changes

2 Answers

The tip to solve this problem is setting the magic this keyword as the 1st argument of static method.

There are 2 use cases with above snippet:

  • As I know, SequelizeModel is the an abstract class so as you extends it you have to return the correct type of init static method which is an instance of a class not a class itself. So the code would look like this:
class Model extends SequelizeModel {
 
  public static init<M extends Model>(this: { new(): M }) { // `M` is an instance
    return new this();
  }
}

class User extends Model {
  baz: number = 1;
}

User.init().baz // result would be an instance of `User`
  • If you wish to return a different static method returning class itself rather than an instance of it:
class Model extends SequelizeModel {
  
  public static yourMethod<M>(this: M) { // `M` is now just a class
    return this;
  }
}

class User extends Model {
  static baz: number = 1;
}

User.init().baz // result would be class of `User`

Edit real code

class Model extends SequelizeModel {
  /**
   *  @default options
   */
  public static options: InitOptions = {
    sequelize,
    timestamps: true,
  };

  /**
   *  @custom options
   */
  public static $options: object = {};

  /**
   *  @model definition
   */
  public static attributes: ModelAttributes = {};

  /**
   *  @init the model
   */
  public static init<M extends Model>(this: { new(): M }) {
    return super.init.call(this, Model.attributes, {
      ...Model.options,
      ...Model.$options,
    });
  }
}
Related