Как реализовать множественное наследование при использовании Flow

Я разрабатываю приложение JS, и я хотел бы поделиться кодом, включая мои классы данных, между интерфейсом и сервером. Я создаю базовый класс с бизнес-логикой, а затем расширяю его версией, подключенной к базе данных, для серверной части и подключенной к AJAX для клиентской части. У меня есть функция, которая принимает базовый класс и возвращает дочерний класс со стандартной функциональностью БД, и этот класс затем может быть расширен другим классом для дополнительных конкретных методов. В основном class DBUser extends DBWrapper(DBUserModel, BaseUser) {}.

В этом коде все работает. Прямо сейчас я пытаюсь реализовать Flow в своей кодовой базе, и он жалуется, что мой дочерний класс не может реализовать интерфейс. Для средства проверки статического кода может быть слишком много динамической композиции.

Я использую Sequelize для базы данных. Экземпляры строки Sequelize содержат все свойства строки, но часть проблемы заключается в том, что существующая типизация Flow, похоже, этого не знает. Таким образом, я должен иметь возможность передать объект DBUserModel вместо UserData, но Flow не позволяет этого.

Вот упрощенный MCVE серверного кода Node.js:

/* @flow */

type BaseData = {
  id?: ?number
};

class BaseClass<TData: BaseData = BaseData> {
  id: ?number;

  constructor(data?: TData) {
    this._initFromData(data);
  }
  _initFromData(data?: TData) {
    if (!data) {
      this.id = null;
      return;
    }
    this.id = data.id;
  }
}

type UserData = {
  id?: ?number,
  name: string,
};

export interface IUser {
  id: ?number;
  name: string;

  _initFromData(data: UserData): void;

  save(): Promise<?IUser>;
  reload(): Promise<?IUser>;
}

class BaseUser extends BaseClass<UserData> implements IUser {
  id: ?number;
  name: string;

  _initFromData(data?: UserData = {}) {
    if (!data.name) {
      throw new Error('User needs a name');
    }
    this.id = data.id;
    this.name = data.name;
  }

  // these methods will be overridden in a child class
  async save() {}
  async reload() {}

  static async getById(id: number) {} // eslint-disable-line no-unused-vars
}

interface DBConnectedClass<TModel, TData> {
  _dbData: ?TModel;

  _initFromData(data: TModel & TData): void;

  save(): Promise<DBConnectedClass<TModel, TData>>;
  reload(): Promise<DBConnectedClass<TModel, TData>>;
}

// actually using Sequelize but mocking it here for a MCVE
class Model {
  update(data) {}
  reload() {}
  get(attr) {}
  static async create(data): Promise<Model> {return new this}
  static async findByPk(id): Promise<Model> {return new this}
}

function dbConnected<TModel: Model, TData: BaseData> (dbClass: Class<TModel>, baseClass: Class<BaseClass<TData>>) {
  return class extends baseClass implements DBConnectedClass<TModel, TData> {
    _dbData: ?TModel;

    constructor(data?: TData & TModel) {
      super(data);
      if (data instanceof dbClass) {
        this._dbData = data;
      }
    }
    _initFromData(data?: TData | TModel) {
      if (data instanceof dbClass) {
        super._initFromData(data.get({plain: true}));
      } else {
        super._initFromData(data);
      }
    }

    async save() {
      if (this._dbData) {
        await this._dbData.update(this);
      }
      if (this.id) {
        let data = (await dbClass.findByPk(this.id): TModel);
        this._dbData = data;
        if (this._dbData) {
          await this._dbData.update(this);
        }
      } else {
        let data: TModel = await dbClass.create(this);
        this._dbData = data;
        this.id = data.get({plain: true}).id;
      }
      return this;
    }

    async reload() {
      if (!this.id) {
        return this;
      }
      if (this._dbData) {
        await this._dbData.reload();
      }
      if (this.id) {
        let data = await dbClass.findByPk(this.id);
        this._dbData = data;
      }
      if (this._dbData) {
        this._initFromData(this._dbData);
      }
      return this;
    }

    static async getById(id: number) {
      const obj: ?TModel = await dbClass.findByPk(id);
      if (!obj) {
        throw new Error('not found');
      }
      return new this(obj);
    }
  }
}

const DBUserModel = Model; // actually it would be a subtype/subclass
class ConnectedUser extends dbConnected<DBUserModel, BaseUser>(DBUserModel, BaseUser) implements IUser, DBConnectedClass<DBUserModel, UserData> {
  // any user-specific DB code goes here
}

А вот и ошибки Flow:

Error ┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈ flow-test.js:138:1

Cannot implement IUser [1] with ConnectedUser because:
 • empty [2] is incompatible with UserData [3] in the first argument of property _initFromData.
 • property name is missing in <<anonymous class>> [4] but exists in IUser [5] in type argument R [6] of the return
   value of property reload.
 • property name is missing in <<anonymous class>> [4] but exists in IUser [7] in type argument R [6] of the return
   value of property save of type argument R [6] of the return value of property reload.
 • in the first argument of property _initFromData:
    • Either UserData [3] is incompatible with TData [8].
    • Or UserData [3] is incompatible with Model [9].
 • property name is missing in ConnectedUser [10] but exists in IUser [1].

           flow-test.js
       [3]  31│   _initFromData(data: UserData): void;
            32│
       [7]  33│   save(): Promise<?IUser>;
       [5]  34│   reload(): Promise<?IUser>;
              :
       [4]  75│   return class extends baseClass implements DBConnectedClass<TModel, TData> {
            76│     _dbData: ?TModel;
            77│
            78│     constructor(data?: TData & TModel) {
            79│       super(data);
            80│       if (data instanceof dbClass) {
            81│         this._dbData = data;
            82│       }
            83│     }
 [2][8][9]  84│     _initFromData(data?: TData | TModel) {
            85│       if (data instanceof dbClass) {
            86│         super._initFromData(data.get({plain: true}));
            87│       } else {
            88│         super._initFromData(data);
            89│       }
              :
           120│       }
           121│       if (this._dbData) {
           122│         this._initFromData(this._dbData);
           123│       }
           124│       return this;
           125│     }
           126│
           127│     static async getById(id: number) {
           128│       const obj: ?TModel = await dbClass.findByPk(id);
           129│       if (!obj) {
           130│         throw new Error('not found');
           131│       }
           132│       return new this(obj);
           133│     }
           134│   }
           135│ }
           136│
           137│ const DBUserModel = Model; // actually it would be a subtype/subclass
   [10][1] 138│ class ConnectedUser extends dbConnected<DBUserModel, BaseUser>(DBUserModel, BaseUser) implements IUser, DBConnectedClass<DBUserModel, UserData> {
           139│   // any user-specific DB code goes here
           140│ }
           141│

           /tmp/flow/flowlib_3f3cb1a7/core.js
       [6] 612│ declare class Promise<+R> {


Error ┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈ flow-test.js:138:1

Cannot implement DBConnectedClass [1] with ConnectedUser because:
 • TModel [2] is incompatible with Model [3] in property _dbData.
 • TModel [4] is incompatible with Model [3] in type argument TModel [5] of type argument R [6] of the return value of
   property reload.
 • TData [7] is incompatible with UserData [8] in type argument TData [9] of type argument R [6] of the return value
   of property reload.
 • property name is missing in BaseData [7] but exists in UserData [8] in type argument TData [9] of type argument
   R [6] of the return value of property reload.
 • in the first argument of property _initFromData:
    • Either Model [3] is incompatible with TData [10].
    • Or UserData [8] is incompatible with TData [10].
    • Or Model [3] is incompatible with TModel [11].
    • Or UserData [8] is incompatible with TModel [11].

           flow-test.js
    [5][9]  56│ interface DBConnectedClass<TModel, TData> {
              :
    [4][7]  75│   return class extends baseClass implements DBConnectedClass<TModel, TData> {
       [2]  76│     _dbData: ?TModel;
              :
  [10][11]  84│     _initFromData(data?: TData | TModel) {
              :
           135│ }
           136│
           137│ const DBUserModel = Model; // actually it would be a subtype/subclass
 [1][3][8] 138│ class ConnectedUser extends dbConnected<DBUserModel, BaseUser>(DBUserModel, BaseUser) implements IUser, DBConnectedClass<DBUserModel, UserData> {
           139│   // any user-specific DB code goes here
           140│ }
           141│

           /tmp/flow/flowlib_3f3cb1a7/core.js
       [6] 612│ declare class Promise<+R> {


Error ┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈ flow-test.js:138:77

Cannot call dbConnected with BaseUser bound to baseClass because UserData [1] is incompatible with BaseUser [2] in type
argument TData [3].

 [3]   7│ class BaseClass<TData: BaseData = BaseData> {
        :
 [1]  37│ class BaseUser extends BaseClass<UserData> implements IUser {
        :
     135│ }
     136│
     137│ const DBUserModel = Model; // actually it would be a subtype/subclass
 [2] 138│ class ConnectedUser extends dbConnected<DBUserModel, BaseUser>(DBUserModel, BaseUser) implements IUser, DBConnectedClass<DBUserModel, UserData> {
     139│   // any user-specific DB code goes here
     140│ }
     141│

Как я могу научить Flow тому, что делает мой код, или немного реорганизовать код, чтобы лучше справляться со статической проверкой?

Используйте класс модели (например,) пользователя, который содержит только поля и не содержит методов (лучше всего, чтобы имя класса модели было похоже на имя таблицы, а поля - на имена столбцов). (конечно, когда-нибудь вы можете добавить несколько методов). Затем создайте СЕРВИС, который модели GET / POST / PUT / DELETE (используя Restful API (google it)).

Kamil Kiełczewski 31.12.2018 00:03

@ KamilKiełczewski Я не уверен, насколько это актуально. Это часть кода, которая должна подключаться к базе данных напрямую, а не через REST API. И класс пользователя действительно содержит в качестве свойств только поля из базы данных. Как я уже сказал, сам код работает, мне нужно набирать для него Flow.

Scimonster 31.12.2018 00:08

может быть, я не понимаю - возможно, вы используете node.js?

Kamil Kiełczewski 31.12.2018 00:11

Да, я использую Node. Только что добавил тег.

Scimonster 31.12.2018 00:13
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Улучшение производительности загрузки с помощью Google Tag Manager и атрибута Defer
Улучшение производительности загрузки с помощью Google Tag Manager и атрибута Defer
В настоящее время производительность загрузки веб-сайта имеет решающее значение не только для удобства пользователей, но и для ранжирования в...
Безумие обратных вызовов в javascript [JS]
Безумие обратных вызовов в javascript [JS]
Здравствуйте! Юный падаван 🚀. Присоединяйся ко мне, чтобы разобраться в одной из самых запутанных концепций, когда вы начинаете изучать мир...
Система управления парковками с использованием HTML, CSS и JavaScript
Система управления парковками с использованием HTML, CSS и JavaScript
Веб-сайт по управлению парковками был создан с использованием HTML, CSS и JavaScript. Это простой сайт, ничего вычурного. Основная цель -...
JavaScript Вопросы с множественным выбором и ответы
JavaScript Вопросы с множественным выбором и ответы
Если вы ищете платформу, которая предоставляет вам бесплатный тест JavaScript MCQ (Multiple Choice Questions With Answers) для оценки ваших знаний,...
0
4
158
1

Ответы 1

Я решил эту проблему, изменив способ подключения к базе данных. Вместо того, чтобы быть производным от базового класса, я использую шаблон миксина для непосредственного изменения дочернего класса. class DBUser extends User {}; dbMixin(DBUserModel, DBUser);

У меня возникла проблема с Flow, который не позволял мне перезаписывать методы в прототипе, но я смог обойти это, выполнив преобразование сначала в any, а затем исправив приведение типа this внутри функции. Немного взломано, но это необходимо сделать.

/* @flow */

type BaseData = {
  id?: ?number
};

class BaseClass<TData: BaseData = BaseData> {
  id: ?number;

  constructor(data?: TData) {
    this._initFromData(data);
  }
  _initFromData(data?: TData) {
    if (!data) {
      this.id = null;
      return;
    }
    this.id = data.id;
  }

  static async getById(id) {}
}

type UserData = BaseData & {
  name: string,
};

export interface IUser {
  id: ?number;
  name: string;

  _initFromData(data: UserData): void;

  save(): Promise<?IUser>;
  reload(): Promise<?IUser>;
}

class BaseUser extends BaseClass<UserData> implements IUser {
  id: ?number;
  name: string;

  _initFromData(data?: UserData = {}) {
    if (!data.name) {
      throw new Error('User needs a name');
    }
    this.id = data.id;
    this.name = data.name;
  }

  // these methods will be overridden in a child class
  async save() {}
  async reload() {}

  static async getById(id: number) {} // eslint-disable-line no-unused-vars
}

interface DBConnectedClass<TModel, TData> {
  id?: ?number;
  _dbData: ?TModel;

  constructor(data: TModel | TData): void;

  _initFromData(data: TModel | TData): void;

  save(): Promise<void>;
  reload(): Promise<void>;
}

// actually using Sequelize but mocking it here for a MCVE
class Model {
  id: ?number;
  name: string;

  update(data) {}
  reload() {}
  get(attr) {}
  static async create(data): Promise<Model> {return new this}
  static async findByPk(id): Promise<Model> {return new this}
}

function dbConnectedMixin<TModel: Model, TData: BaseData> (dbClass: Class<TModel>, baseClass: Class<DBConnectedClass<TModel, TData>>) {
  const _initFromData = baseClass.prototype._initFromData;
  (baseClass: any).prototype._initFromData = function(data?: TData | TModel) {
    const self = (this: DBConnectedClass<TModel, TData>);
    if (data instanceof dbClass) {
      self._dbData = data;
    }
    _initFromData.call(self, data);
  };

  // const save = baseClass.prototype.save;
  (baseClass: any).prototype.save = async function() {
    const self = (this: DBConnectedClass<TModel, TData>);
    if (self._dbData) {
      await self._dbData.update(self);
    }
    if (self.id) {
      let data = (await dbClass.findByPk(self.id): TModel);
      self._dbData = data;
      if (self._dbData) {
        await self._dbData.update(self);
      }
    } else {
      let data: TModel = await dbClass.create(self);
      self._dbData = data;
      self.id = data.get({plain: true}).id;
    }
  };

  // const reload = baseClass.prototype.reload;
  (baseClass: any).prototype.reload = async function() {
    const self = (this: DBConnectedClass<TModel, TData>);
    if (!self.id) {
      return;
    }
    if (self._dbData) {
      await self._dbData.reload();
    }
    if (self.id) {
      let data = await dbClass.findByPk(self.id);
      self._dbData = data;
    }
    if (self._dbData) {
      self._initFromData(self._dbData);
    }
  };

  (baseClass: any).getById = async function(id: number) {
    const self = (this: Class<DBConnectedClass<TModel, TData>>);
    const obj: ?TModel = await dbClass.findByPk(id);
    if (!obj) {
      throw new Error('not found');
    }
    return new self(obj);
  };
}

const DBUserModel = Model; // actually it would be a subtype/subclass
class ConnectedUser extends BaseUser implements IUser, DBConnectedClass<DBUserModel, UserData> {
  _dbData: ?DBUserModel;

  // any user-specific DB code goes here
}
dbConnectedMixin<DBUserModel, UserData>(DBUserModel, ConnectedUser);

Другие вопросы по теме