Я разрабатываю приложение 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 тому, что делает мой код, или немного реорганизовать код, чтобы лучше справляться со статической проверкой?
@ KamilKiełczewski Я не уверен, насколько это актуально. Это часть кода, которая должна подключаться к базе данных напрямую, а не через REST API. И класс пользователя действительно содержит в качестве свойств только поля из базы данных. Как я уже сказал, сам код работает, мне нужно набирать для него Flow.
может быть, я не понимаю - возможно, вы используете node.js?
Да, я использую Node. Только что добавил тег.



![Безумие обратных вызовов в javascript [JS]](https://i.imgur.com/WsjO6zJb.png)


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