Как написать универсальный хук слушателя событий в React с выводом типов?

Вопрос или проблема

Я пытаюсь написать универсальный React-хук, который можно использовать с любым генератором событий, соответствующим определенному интерфейсу:

import { useEffect } from 'react'

type EventsMap = Record<string, (...args: any) => void>
type EventName<EM extends EventsMap> = keyof EM
type EventListener<EM extends EventsMap, EN extends EventName<EM>> = EM[EN]

export type EventEmitter<EM extends EventsMap> = {
  on<K extends keyof EM>(event: K, cb: EM[K]): () => void
}

export function useEventListener<
  EM extends EventsMap,
  EN extends EventName<EM>,
>(
  emitter: EventEmitter<EM> | undefined | null,
  eventName: EN,
  listener: EventListener<EM, EN>,
) {
  useEffect(() => {
    return emitter?.on(eventName, listener)
  })
}

Вышеуказанный код работает, но типы корректны только в том случае, если я вручную указываю их при вызове:

  useEventListener<MyEventMap, keyof MyEventMap>(emitter, 'myEvent', () => {
    console.log('привет')
  })

По какой-то причине TypeScript принимает генератор событий как экземпляр генератора событий, но не может вывести EventMap из него, чтобы ограничить название события.

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

Ответ или решение

Вопрос, который вы подняли, действительно интересный и связан с типизацией в TypeScript, когда мы говорим о.generic-hook для событий. Давайте разберем вашу проблему и предложим решение, которое должно позволить вашему хуку useEventListener правильно выводить тип события из переданного эмиттера.

Основная причина, по которой TypeScript не может вывести тип EM из параметра emitter, заключается в том, что в текущей версии типа не определены четкие связи между параметрами, что затрудняет TypeScript вывести типы. Мы можем использовать параметры типов более эффективно, чтобы решить эту проблему.

Вот предложенное решение для вашего хука useEventListener, которое должно корректно обрабатывать типы:

import { useEffect } from 'react';

type EventsMap = Record<string, (...args: any[]) => void>;
type EventName<EM extends EventsMap> = keyof EM;
type EventListener<EM extends EventsMap, EN extends EventName<EM>> = EM[EN];

export type EventEmitter<EM extends EventsMap> = {
  on<K extends keyof EM>(event: K, cb: EM[K]): () => void;
};

export function useEventListener<
  EM extends EventsMap,
  EN extends EventName<EM>
>(
  emitter: EventEmitter<EM> | null | undefined,
  eventName: EN,
  listener: EventListener<EM, EN>,
) {
  useEffect(() => {
    if (!emitter) return; // Проверка на существование эмиттера.

    const unsubscribe = emitter.on(eventName, listener);
    return () => unsubscribe();
  }, [emitter, eventName, listener]);
}

// Пример использования с правильной типизацией
interface MyEventMap {
  myEvent: (msg: string) => void;
}

const myEmitter: EventEmitter<MyEventMap> = {
  on: (event, cb) => {
    // Реализация подписки на событие
    return () => {
      // Логика отписки
    };
  },
};

// Теперь TypeScript выводит правильные типы
useEventListener(myEmitter, 'myEvent', (msg) => {
  console.log('Received message:', msg);
});

Пояснения к изменениям:

  1. Отказ от явной типизации аргументов: Теперь при вызове useEventListener TypeScript сможет самостоятельно вывести тип EM из переданного эмиттера. Это достигается благодаря тому, что аргумент emitter используется для вывода типа в других аргументах.

  2. Проверка на существование эмиттера: Это важно, так как мы избегаем возможных ошибок при попытке подписаться на событие, если эмиттер не был передан.

  3. Отмена подписки: Правильная отписка от события при размонтировании компонента добавляет надежности в использование вашего хуку.

Теперь, когда вы вызываете useEventListener, TypeScript будет автоматически выводить типы, основанные на переданном эмиттере, что упростит использование вашего хука и сделает его более типобезопасным.

Оцените материал
Добавить комментарий

Капча загружается...