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

Я использую Typescript для записи конечных точек REST API в качестве функций через Firebase, и все методы следуют аналогичному шаблону: проверьте request.body, извлеките соответствующие данные из этих данных тела, поместите их в строго типизированный объект, использование этого объекта для передачи данных в базу данных с помощью некоторого кода доступа к данным. Написав одну и ту же базовую логику извлечения данных несколько раз для работы с request.body, я решил, что должен быть способ абстрагироваться от этой работы. Для этого у меня есть три требования: (1) метод должен работать для извлечения данных из request.body для любой из моих моделей данных. (2) Модели данных должны быть полностью информативными, чтобы они не только описывали свойства, которыми должны обладать данные, но и могли связываться, когда требуется определенный набор свойств. (3) Метод должен уметь определять из моделей данных, какие свойства требуются, и выполнять некоторую проверку данных, передаваемых через request.body.

В качестве примера №2 и модели являются информативными: подумайте, например, что когда я создаю новую запись данных, мне не нужен идентификатор, поскольку, если его нет, я могу создать его в функции и передать обратно. С другой стороны, свойство "name" является обязательным в этом случае. В отличие от этого, метод update требует ID записи (чтобы он знал, какую запись обновлять), но не требует "name", если только это не то, что на самом деле изменен.

Мой подход заключался в использовании (1) статического фабричного метода в отдельном классе, который принимает тип класса для модели данных, которую необходимо создать; предполагаемая операция (например, создать, прочитать, обновить или удалить); и тело запроса. (2) Набор классов модели данных, которые в основном просто описывают данные и включают небольшую логику проверки, где это необходимо, но также включают (статический) список имен полей и связанных значений требований (хранящихся в виде четырех битов, где каждая позиция представляет собой один из четырех операций CRUD.) (3) Общий интерфейс, так что статический фабричный метод знает, как работать с различными объектами данных, чтобы получить эти имена полей и флаги использования.

Вот мой статический фабричный метод:

static create<T extends typeof DataObjectBase>(cls: { new(...args: any[]): T; }, intendedOperation: number, requestBody: any) : T {
        let dataObject : T = null;
        const sourceData = {};
        const objFields = cls.fieldNames;
        const flagCollection = cls.requiredUseFlags();
        const requiredFields = flagCollection.getFieldsForOperation(intendedOperation);
        if (requestBody) {
            // parse the request body
            // first get all values that are available and match object field names
            const allFields = Object.values(objFields); // gets all properties as key/value pairs for easier iteration
            // iterate through the allFields array
            for (const f in allFields) {
                if (requestBody.hasOwnProperty(f)) {
                    // prop found; add the field to 'sourceData' and copy the value from requestBody
                    sourceData[f] = requestBody[f];
                } else if (requiredFields.indexOf(f)>-1) {
                    // field is required but not available; throw error
                    throw new InvalidArgumentError(`${cls}.${f} is a required field, but no value found for it in request.body.`, requestBody);
                }
            }
            dataObject = (<any>Object).assign(dataObject, sourceData);
        } else {
            throw new ArgumentNullError('"requestBody" argument cannot be null.', requestBody);
        }
        return new cls();
    }

Вот пример класса модели данных:

export class Address extends DataObjectBase {
    constructor(
        public id         : string,
        public street1    : string,
        public street2    : string = "",
        public city       : string,
        public state      : string,
        public zip        : string) {
        // call base constructor
        super();
    }

    static fieldNames = {
        ID      = "id",
        STREET1 = "street1",
        STREET2 = "street2",
        // you get the idea...
    }

    static requiredUseFlags() {
        ID = READ | UPDATE | DELETE,
        STREET1 = 0,
        // again, you get the idea...
        // CREATE, READ, UPDATE, DELETE are all bit-flags set elsewhere
    }
}

Я хочу иметь возможность вызывать указанный выше метод create следующим образом:

const address = create<Address>(Address, CREATE, request.body);

Изначально я пробовал такую ​​подпись:

static create<T extends typeof DataObjectBase>(cls: T, intendedOperation: number, requestBody: any) : T

Однако, когда я сделал это, я получил сообщение об ошибке: «Адрес - это тип, но используется как значение». Как только я изменил его на то, что у меня было выше, я перестал получать эту ошибку и начал получать Property 'fieldNames' does not exist on type 'new (...args: any[]) => T'

Примечание. Я также пробовал использовать два интерфейса для описания (в первом) методов экземпляра и (во втором) статических методов, а затем заставить статический интерфейс расширять интерфейс экземпляра, а базовый класс реализует статический интерфейс и т. д., как описано здесь, здесь и здесь. Это тоже не совсем меня туда привело.

Я, конечно, готов признать, что вполне мог все это перестроить, и я рад принять другие, более простые предложения. Но я полагаю, что должен быть способ достичь того, что я хочу, и избежать необходимости писать один и тот же базовый код для анализа тела запроса снова и снова.


person bubba    schedule 04.07.2019    source источник
comment
DataObjectBase абстрактный?   -  person Titian Cernicova-Dragomir    schedule 05.07.2019
comment
Это не абстрактно. У меня есть несколько общих методов, связанных с данными, которые используются во всех его подклассах.   -  person bubba    schedule 05.07.2019
comment
В любом случае решение ниже должно работать   -  person Titian Cernicova-Dragomir    schedule 05.07.2019


Ответы (1)


Вы можете использовать this внутри статического метода для ссылки на текущий класс (позволяя вам писать new this() для создания экземпляра класса).

Что касается ввода его таким образом, чтобы и создавать объекты, и иметь доступ к статике, самое простое решение - иметь подпись конструктора, как вы ее определили, и добавить статику обратно, используя пересечение с Pick<typeof DataObjectBase, keyof typeof DataObjectBase>. Это сохранит статические члены, но удалит все сигнатуры конструктора базового класса.

Также T должен расширять DataObjectBase (тип экземпляра), а не typeof DataObjectBase (тип класса)

type FieldsForOperation = {getFieldsForOperation(intendedOperation: number): string[] }
class DataObjectBase {
static fieldNames: Record<string, string>
static requiredUseFlags():FieldsForOperation { return null!; }
static create<T extends DataObjectBase>(this: (new (...a: any[]) => T) & Pick<typeof DataObjectBase, keyof typeof DataObjectBase> , intendedOperation: number, requestBody: any) : T {
        let dataObject : T = null;
        const sourceData = {};
        const objFields = this.fieldNames;
        const flagCollection = this.requiredUseFlags();
        // rest of code
        return new this();
    }
}

export class Address extends DataObjectBase {
    constructor(
        public id         : string,
        public street1    : string,
        public street2    : string = "",
        public city       : string,
        public state      : string,
        public zip        : string) {
        // call base constructor
        super();
    }

    static fieldNames = {
        "": ""
    }

    static requiredUseFlags(): FieldsForOperation {
        return null!;
    }
}

Address.create(0, {})

Примечание: просто исправление TS не приведет к заключению мнения о чрезмерно инженерной части ????

person Titian Cernicova-Dragomir    schedule 04.07.2019
comment
Чемпион народов - person Shanon Jackson; 05.07.2019
comment
@titian Большое спасибо за ваш ответ! Работаем над этим сейчас. А пока у меня вопрос по этой строке: typescript static requiredUseFlags():FieldsForOperation { return null!; } Что означает ноль! делать? Я искал это и не нашел ничего полезного. - person bubba; 06.07.2019
comment
@bubba - это просто заполнитель для реализации. Я возвращаю null, но при строгих null проверках null не может быть назначен FieldsForOperation, поэтому я утверждаю, что null не null с ненулевым утверждением (!), заглушающим ошибку - person Titian Cernicova-Dragomir; 06.07.2019