Как я могу реализовать в TypeScript типизированную функцию "обоих"? - PullRequest
1 голос
/ 29 мая 2020

Сборник рассказов действия аддон предоставляет удобный способ регистрации вызовов обратного вызова:

renderButton({onClick: action('onClick')});

Вызов action возвращает функцию, которая регистрирует строку (onClick) и аргументы, с которыми он вызывается.

Иногда я хочу иметь и action, и что-то еще:

renderButton({
  onClick: (arg1, arg2, arg3) => {
    action('onClick')(arg1, arg2, arg3);
    // ... my code
  }
});

Вызов action стал более подробным, так как я должен передать все аргументы. Итак, я реализовал функцию both, позволяющую мне писать:

renderButton({
  onClick: both(action('onClick'), (arg1, arg2, arg3) => {
    // ... my code
  })
});

Это отлично работает, пока я не попытаюсь добавить типы TypeScript. Вот моя реализация:

function both<T extends unknown[], This extends unknown>(
  a: (this: This, ...args: T) => void,
  b: (this: This, ...args: T) => void,
): (this: This, ...args: T) => void {
  return function(this: This, ...args: T) {
    a.apply(this, args);
    b.apply(this, args);
  };
}

Этот тип проверяет и работает во время выполнения, но в зависимости от контекста он либо приводит к нежелательным типам any, либо к ошибкам типов. Например:

const fnWithCallback = (cb: (a: string, b: number) => void) => {};

fnWithCallback((a, b) => {
  a;  // type is string
  b;  // type is number
});

fnWithCallback(
  both(action('callback'), (a, b) => {
    a;  // type is any
    b;  // type is any
  }),
);

fnWithCallback(
  both(action('callback'), (a, b) => {
//                         ~~~~~~~~~~~
// Argument of type '(a: T[0], b: T[1]) => number' is not assignable to
// parameter of type '() => void'.
  }),
);

Возможно ли, чтобы both правильно захватил типы аргументов из контекста обратного вызова? И чтобы избежать типов any, которые предположительно возникают из объявления action :

export type HandlerFunction = (...args: any[]) => void;

Вот ссылка на игровую площадку с полным примером.

1 Ответ

2 голосов
/ 29 мая 2020

Это должно работать и поддерживать правильные типы аргументов обратного вызова:

type UniversalCallback<Args extends any[]> = (...args: Args) => void;
function both<
    Args extends any[],
    CB1 extends UniversalCallback<Args>,
    CB2 extends UniversalCallback<Args>
>(fn1: CB1, fn2: CB2): UniversalCallback<Args> {
  return (...args:Args) => {
    fn1(...args);
    fn2(...args);
  };
}

Это решение игнорирует this, но я не знаю, является ли это проблемой для вас, потому что приведенные вами примеры использования не действительно используйте this.

Достаточно легко расширить поддержку для передачи this для обратных вызовов:

type UniversalCallback<T, Args extends any[]> = (this:T, ...args: Args) => void;
function both<
    T,
    Args extends any[],
    CB1 extends UniversalCallback<T, Args>,
    CB2 extends UniversalCallback<T, Args>
>(fn1: CB1, fn2: CB2): UniversalCallback<T, Args> {
  return function(this:T, ...args:Args) {
    fn1.apply(this, args);
    fn2.apply(this, args);
  };
}

Он отлично работает в следующем тесте:

class A { f() {} }

const fnWithCallback = (cb: (this: A, a: string, b: number) => void) => { };

fnWithCallback(function(a, b) {
  a;  // type is string
  b;  // type is number
  this.f(); // works
});

fnWithCallback(
  both(function (a) {
    a; // correct type
    this.f(); // works
  }, function (a, b) {
    a; b; // correct types
    this.f(); // works
  }),
);
...