Как правильно набрать обработчик события в завернутом вызове addEventListener в TypeScript - PullRequest
2 голосов
/ 28 января 2020

Я пытаюсь написать слой абстракции для метода addEventListener, но сталкиваюсь с проблемой печати. В приведенном ниже минимальном воспроизведении

function addEventListener<T extends EventTarget>(
  element: T,
  type: string,
  handler: EventListener
) {
  element.addEventListener(type, handler);
}

addEventListener<Window>(window, "mousedown", (event: MouseEvent) => {
  console.log(event.pageX);
});

TypeScript жалуется, что тип параметра несовместим с MouseEvent:

Argument of type '(event: MouseEvent) => void' is not assignable to parameter of type 'EventListener'.
  Types of parameters 'event' and 'evt' are incompatible.
    Type 'Event' is missing the following properties from type 'MouseEvent': altKey, button, buttons, clientX, and 20 more.

Я знаю, что следующий (базовый c) прослушиватель событий:

window.addEventListener('mousedown', (event: MouseEvent) => {
  console.log(event.pageX);
});

работает просто отлично, поэтому я предполагаю, что что-то не так при наборе параметра handler как EventListener, но я не могу понять, каким он должен быть. Большинство ответов, с которыми я столкнулся Похоже, что c для React, что не имеет значения в моем случае.

Выше codenippet: TypeScript Playground | CodeSandbox

1 Ответ

1 голос
/ 28 января 2020

Проблема в том, что ваша addEventListener() не знает, как параметр type относится к подтипу Event, который handler должен принять. В стандартном библиотечном файле TypeScript lib.dom.d.ts отдельным EventTarget типам дано очень точное отображение c от type до handler. Например, сигнатура интерфейса Window для его addEventListener метода выглядит следующим образом :

addEventListener<K extends keyof WindowEventMap>(
  type: K, 
  listener: (this: Window, ev: WindowEventMap[K]) => any, 
  options?: boolean | AddEventListenerOptions
): void;

, где WindowEventMap - это , определенное здесь как большое отображение между type именами и Event типами. В частности, для ключа mousedown тип значения свойства равен MouseEvent. Таким образом, когда вы вызываете

window.addEventListener('mousedown', (event: MouseEvent) => {
  console.log(event.pageX);
});

, все работает, потому что K выводится как "mousedown", и поэтому параметр handler ожидает MouseEvent.


Без этого В зависимости от типа отображаемой информации, типизация выглядит следующим образом: type равно string, а listener равно (event: Event)=>void. Но, как вы видели, вы не можете просто передать (event: MouseEvent)=>void параметру, ожидающему (event: Event)=>void, потому что это вообще небезопасно. Если вы дадите мне функцию, которая принимает только MouseEvent с, я не смогу использовать ее как функцию, которая принимает все Event с. Если я попытаюсь, и функция, которую вы мне дадите, получит доступ к чему-то вроде свойства pageX, я могу получить ошибку времени выполнения.

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

Вместо этого, если вы хотите ослабить ввод только для функции addEventListener, вы можете сделать ее обобщенной. c в подтипе Event, например:

function addEventListener<T extends EventTarget, E extends Event>(
  element: T, type: string, handler: (this: T, evt: E) => void) {
  element.addEventListener(type, handler as (evt: Event) => void);
}

Тогда ваш код скомпилируется:

addEventListener(window, "mousedown", (event: MouseEvent) => {
  console.log(event.pageX);
});

Но помните: эта типизация слишком свободна, чтобы перехватывать ошибки, например как это:

addEventListener(document.createElement("div"), "oopsie",
  (event: FocusNavigationEvent) => { console.log(event.originLeft); }
);

Если вы попробуете это прямо на HTMLDivElement, вы получите ошибку:

document.createElement("div").addEventListener("oopsie",
  (event: FocusNavigationEvent) => { console.log(event.originLeft); }
); // error! oopsie is not acceptable

, которая не будет решена, пока вы не исправите оба type параметр

document.createElement("div").addEventListener("blur",
  (event: FocusNavigationEvent) => { console.log(event.originLeft); }
); // error! FocusNavigationEvent is not a FocusEvent

И использовал соответствующий Event подтип

document.createElement("div").addEventListener("blur",
  (event: FocusEvent) => { console.log(event.relatedTarget); }
); // okay now

Итак, это примерно настолько, насколько я могу go здесь. Вы можете попытаться сделать огромное сопоставление из всех известных EventTarget типов со всеми правильными type параметрами для каждой цели, а затем со всеми правильными Event подтипами для каждого EventTarget / type пары и имеют единственную обобщенную функцию c addEventListener, которая работает ... но по размеру она будет сравнима с файлом lib.dom.d.ts, и я не склонен тратить на это слишком много времени. Если вам нужна безопасность типов, вам, вероятно, лучше не использовать эту конкретную абстракцию. В противном случае, если у вас все в порядке с компилятором, пропускающим некоторые несоответствия, то приведенная выше типизация может быть способом go.


Хорошо, надеюсь, это поможет; удачи!

ссылка на игровую площадку

...