Класс, заключающий в себе функцию: получить правильные наборы и доступ к количеству аргументов (в машинописи) - PullRequest
0 голосов
/ 27 апреля 2018

Я написал небольшой класс, который упаковывает указанную функцию, а также принимает список шаблонов проверки для каждого аргумента функции. Этот класс имеет функцию call, которая должна принимать те же аргументы, что и функция ввода, проверять их, вызывать функцию ввода и переносить результат, например. в rxjs / наблюдаемый. Следовательно, конструктор должен проверить, совпадает ли число аргументов, указанное функцией method.length, с количеством шаблонов проверки.

Необработанная версия JavaScript должна выглядеть примерно так:

class Wrapper {
    method;
    validationPatterns;

    constructor(method, validationPatterns) {
        if (method.length !== validationPatterns.length) {
            // throw error
        }
        this.method = method;
        this.validationPatterns = validationPatterns;
    }

    validate(...args) {
        this.validationPatterns.forEach(pattern => {
            // apply validation pattern
        });
    }

    call(...args) {
        // validate arguments
        this.validate(...args);
        // run method and wrap its result e.g. in an Observable
        const methodResult = this.method(...args);
        return Observable.of(methodResult);
    }
}

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

  1. доступ к числу параметров функции (через method.length) внутри конструктора. Поэтому невозможно создать экземпляр класса с ложной проверкой.
  2. и в то же время иметь правильную типизацию для всех функций класса.

Я пробовал следующие вещи:

1. Использование <T extends Function>

Плюсы:

  • Я могу получить доступ к указанному методу внутри класса и в классе экземпляры с правильными типами.

Минусы:

  • Нет способа извлечь типы параметров и вернуть тип из T и примените их к validate() и call(). У них еще есть использовать типы any / any[].

Пример кода:

class Wrapper<F extends Function> {

    constructor(public method: F, public validationPatterns: any[]) {
        if (method.length !== validationPatterns.length) {
            // throw error
        }
    }

    validate(...args: any[]): void {
        // ...
    }

    call(...args: any[]): any {
        // ...
    }
}

2. Использование одного параметра функции.

Плюсы:

  • Таким образом, можно применять правильные типы ко всем функциям.

Минусы:

  • Функции, которым требуется более одного аргумента, должны обернуть их внутри правильно набранного кортежа или объекта. Это требует дополнительного синтаксиса (например, скобки [a, b]) при написании и вызове метода. Но это все равно должно быть приемлемым.
  • Однако мы не можем проверить количество шаблонов проверки, потому что method.length == 1. Следовательно, ради полной типизации мы должны были бы признать недостатки функциональности, что мне не нравится.

Пример кода:

class Wrapper<T, U> {
    constructor(public method: (arg: T) => U, public validationPatterns: any[]) {
        // CANNOT check correct number of validation patterns
    }

    validate(args: T): void {
        // ...
    }

    call(args: T): Observable<U> {
        // ...
    }
}

3. Перегрузки

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

У меня нет опыта с перегрузкой в ​​машинописи, но я думаю, пример кода с независимо перегруженными функциями может выглядеть примерно так:

class Wrapper<F extends Function, T1, T2, T3, U> {

    constructor(method: (arg1: T1, arg2: T2, arg3: T3) => U, validationPatterns: any[]);
    constructor(method: (arg1: T1, arg2: T2) => U, validationPatterns: any[]);
    constructor(method: (arg1: T1) => U, validationPatterns: any[]);
    constructor(method: () => U, validationPatterns: any[]);
    constructor(public method: F, public validationPatterns: any[]) {
        // ...
    }

    validate(arg1: T1, arg2: T2, arg3: T3): void;
    validate(arg1: T1, arg2: T2): void;
    validate(arg1: T1): void;
    validate(): void;
    validate(...args: any[]): void {
        // ...
    }

    call(arg1: T1, arg2: T2, arg3: T3): Observable<U>;
    call(arg1: T1, arg2: T2): Observable<U>;
    call(arg1: T1): Observable<U>;
    call(): Observable<U>;
    call(...args: any[]): Observable<U> {
        // ...
    }
}

Вопросы

  • Есть ли способ получить и то и другое: полный набор и доступ к номеру аргумента функции?
  • Можно ли использовать перегрузку так, как я собираюсь?
  • Какой подход вы мне порекомендуете? В настоящее время я иду с первым (<T extends Function>), который имеет полную функциональность, но отсутствует типы.

Спасибо!

1 Ответ

0 голосов
/ 27 апреля 2018

Вы можете перегружать классы, но это немного сложнее, вам нужно объявлять перегрузки отдельно и определять конструктор для каждой перегрузки:

class WrapperImpl {

    constructor(public method: Function, public validationPatterns?: Function[]) {
        // ...
    }
    validate(...args: any[]): void {
        // ...
    }

    call(...args: any[]): Observable<any> {
        return new Observable<any>();
    }
}


type KeysOfUnion<T> = T extends any ? keyof T: never;
type IsValid<T, TResult> = KeysOfUnion<T> extends never ?  never : TResult;
const Wrapper: {
    new <U>  (m: ()=> U, ) :  {
        validate(): void
        call(): Observable<U>
    }
    new <T1, U>  (m: (a: T1)=> U, validationPatterns: [IsValid<T1, (a: T1)=>boolean>]) :  {
        validate(a: T1): void
        call(a: T1): Observable<U>
    }
    new <T2, T1, U>  (m: (a: T1, a2: T2)=> U, validationPatterns: [IsValid<T1, (a: T1)=>boolean>, IsValid<T2, (a: T2)=>boolean>]) :  {
        validate(a: T1, a2: T2): void
        call(a: T1, a2: T2): Observable<U>
    }
    // Add more as needed 
} = WrapperImpl

let w = new Wrapper(() => "");
w.call() // return Observable<string>

let w2 = new Wrapper((n: number) => "", [n=> true]);
w2.call(10) // return Observable<string>

Я бы не стал слишком сильно беспокоиться об обобщениях в реализации, в любом случае вам нужно будет использовать очень общие типы (т. Е. Function и any). Обобщения в этом вам мало помогут. Звонки будут правильно набраны и проверены, и это важная часть.

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

Этот подход все еще имеет по крайней мере один недостаток, когда используется с функциями с необязательными параметрами, поскольку необязательные параметры становятся обязательными:

function withOpt(n?: number) { return ""}
let w3 = new Wrapper(withOpt, [n=> true]);
w3.call(1) // 1 is required.
...