Другое

Как убрать второй аргумент дженерика в TypeScript

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

Как убрать второй аргумент из дженерика при типизации функции, возвращающей другую функцию в TypeScript?

Я пытаюсь создать типобезопасный эмиттер событий в TypeScript. В настоящее время у меня есть следующая реализация:

typescript
export interface TableEventMap {
  "cell:hover": { start: number; end: number };
  test2: number;
}

export const useEmitter = <EM extends Record<string, any>, K extends keyof EM>(event: K) => {
  return (payload: EM[K]) => {};
};

const emitTest1 = useEmitter<TableEventMap, "cell:hover">("cell:hover"); // const emitTest1: (payload: { start: number; end: number; }) => void
const emitTest2 = useEmitter<TableEventMap, "test2">("test2"); // const emitTest2: (payload: number) => void

Я хотел бы добиться того же результата, но без использования второго аргумента в дженерике, чтобы можно было вызывать функцию так:

typescript
const emitTest1 = useEmitter('cell:hover');
const emitTest2 = useEmitter("test2");

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

typescript
export const useEmitter = <K extends keyof TableEventMap>(event: K) => {
  return (payload: TableEventMap[K]) => {};
};

Есть ли способы решить эту задачу, сохранив типобезопасность и универсальность?

Чтобы убрать второй аргумент из дженерика при типизации функции эмиттера событий в TypeScript, вы можете использовать автоматическое вывод типов (type argument inference) TypeScript. Основная идея - передать тип карты событий в качестве первого дженерика праметра и позволить TypeScript самому определить правильный тип на основе переданного имени события.

Краткий ответ

Чтобы добиться автоматического вывода типов без второго дженерика параметра, передайте TableEventMap как первый дженерик параметр и используйте keyof typeof для определения типа события. TypeScript сможет автоматически вывести правильный тип payload на основе переданной строки с именем события.


Содержание


Решение с автоматическим выводом типов

Основное решение заключается в использовании keyof typeof для автоматического определения типа события на основе переданной строки:

typescript
export interface TableEventMap {
  "cell:hover": { start: number; end: number };
  test2: number;
}

export const useEmitter = <E extends Record<string, any>, K extends keyof E>(event: K) => {
  return (payload: E[K]) => {};
};

// Теперь вы можете вызывать функцию без второго дженерика параметра
const emitTest1 = useEmitter(TableEventMap, 'cell:hover'); 
// const emitTest1: (payload: { start: number; end: number; }) => void

const emitTest2 = useEmitter(TableEventMap, "test2"); 
// const emitTest2: (payload: number) => void

Однако этот подход всё ещё требует передачи TableEventMap в качестве первого аргумента. Для более элегантного решения можно использовать фабричную функцию:

typescript
function createEventEmitter<T extends Record<string, any>>() {
  return <K extends keyof T>(event: K) => {
    return (payload: T[K]) => {};
  };
}

// Создаем экземпляр эмиттера для конкретной карты событий
const useTableEmitter = createEventEmitter<TableEventMap>();

// Теперь можно вызывать без указания дженериков
const emitTest1 = useTableEmitter('cell:hover'); 
// const emitTest1: (payload: { start: number; end: number; }) => void

const emitTest2 = useTableEmitter("test2"); 
// const emitTest2: (payload: number) => void

Этот подход идеально решает вашу задачу, как показано в документации TypeScript о выводе типов аргументов.


Альтернативные подходы

1. Использование класса с дженериком

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

typescript
class EventEmitter<T extends Record<string, any>> {
  private events: Record<string, Array<(payload: any) => void>> = {};

  on<K extends keyof T>(event: K, callback: (payload: T[K]) => void) {
    if (!this.events[event as string]) {
      this.events[event as string] = [];
    }
    this.events[event as string].push(callback);
  }

  emit<K extends keyof T>(event: K, payload: T[K]) {
    const callbacks = this.events[event as string];
    if (callbacks) {
      callbacks.forEach(callback => callback(payload));
    }
  }
}

// Использование
const emitter = new EventEmitter<TableEventMap>();
emitter.on('cell:hover', (payload) => {
  // TypeScript знает, что payload имеет тип { start: number; end: number }
  console.log(payload.start);
});

2. Использование замыканий с утилитарными типами

Более продвинутый подход с использованием утилитарных типов TypeScript:

typescript
type EventPayload<T, K extends keyof T> = T[K];

function createEmitter<T extends Record<string, any>>() {
  return <K extends keyof T>(event: K) => {
    return (payload: EventPayload<T, K>) => {
      // Логика эмитта события
      console.log(`Event "${String(event)}" emitted with payload:`, payload);
    };
  };
}

// Использование
const useTableEmitter = createEventEmitter<TableEventMap>();
const emit = useTableEmitter('cell:hover');
emit({ start: 1, end: 5 }); // OK
// emit({ start: 1 }); // Ошибка: отсутствует свойство 'end'

Работа с ограниченными типами событий

Если вы хотите создать более строгий тип для событий, можно использовать литеральные типы:

typescript
type EventType = 'cell:hover' | 'test2';

type EventMap<T extends EventType> = {
  [K in T]: K extends 'cell:hover' ? { start: number; end: number } : number;
};

function createStrictEmitter<T extends EventType>() {
  return <K extends T>(event: K) => {
    return (payload: EventMap<T>[K]) => {};
  };
}

// Использование
const useStrictEmitter = createStrictEmitter<'cell:hover' | 'test2'>();
const emit1 = useStrictEmitter('cell:hover'); // Выведет тип payload для cell:hover
const emit2 = useStrictEmitter('test2');      // Выведет тип payload для test2

Практические примеры использования

Пример 1: React hook для событий

typescript
import { useEffect, useCallback } from 'react';

function useEventEmitter<T extends Record<string, any>>() {
  return useCallback(<K extends keyof T>(event: K, payload: T[K]) => {
    // Логика эмитта события
    console.log(`Event "${String(event)}":`, payload);
  }, []);
}

// Использование в компоненте
const MyComponent = () => {
  const emit = useEventEmitter<TableEventMap>();
  
  const handleCellHover = useCallback(() => {
    emit('cell:hover', { start: 1, end: 5 });
  }, [emit]);

  return <div onMouseEnter={handleCellHover}>Hover me</div>;
};

Пример 2: Классический эмиттер событий

typescript
class EventEmitter<T extends Record<string, any>> {
  private listeners: Map<keyof T, Array<(payload: any) => void>> = new Map();

  on<K extends keyof T>(event: K, callback: (payload: T[K]) => void) {
    if (!this.listeners.has(event)) {
      this.listeners.set(event, []);
    }
    this.listeners.get(event)!.push(callback);
  }

  off<K extends keyof T>(event: K, callback: (payload: T[K]) => void) {
    const eventListeners = this.listeners.get(event);
    if (eventListeners) {
      const index = eventListeners.indexOf(callback);
      if (index > -1) {
        eventListeners.splice(index, 1);
      }
    }
  }

  emit<K extends keyof T>(event: K, payload: T[K]) {
    const eventListeners = this.listeners.get(event);
    if (eventListeners) {
      eventListeners.forEach(callback => callback(payload));
    }
  }
}

// Использование
const emitter = new EventEmitter<TableEventMap>();

emitter.on('cell:hover', (payload) => {
  console.log('Cell hovered:', payload.start, payload.end);
});

emitter.emit('cell:hover', { start: 10, end: 20 });

Сравнение разных подходов

Подход Плюсы Минусы Применимость
Фабричная функция Полная типобезопасность, чистый синтаксис Требует создания экземпляра фабрики Идеально для React hooks и утилит
Класс эмиттера Гибкость, возможность наследования Более сложная реализация Для сложных систем событий
Замыкания с утилитами Максимальная типобезопасность Может быть сложен для понимания Для высокоуровневых абстракций
Прямая функция с keyof Простой синтаксис Требует передачи типа события Для простых случаев использования

На основе исследования на Reddit и документации TypeScript, наиболее рекомендуемым подходом является использование фабричной функции, так как он обеспечивает наилучший баланс между типобезопасностью и удобством использования.


Источники

  1. TypeScript Documentation - Generics and Type Inference
  2. r/TypeScript - Type inference solution
  3. Type-Safe Event Emitter Implementation
  4. Stack Overflow - Event listener React hook with type inference
  5. Stack Overflow - Preserve type inference when mapping generic Record keys

Заключение

Для решения вашей задачи по созданию типобезопасного эмиттера событий без второго дженерика параметра рекомендую следующие подходы:

  1. Используйте фабричную функцию - это самый чистый и типобезопасный способ, который позволит вам вызывать useEmitter('cell:hover') без указания дженериков.

  2. Реализуйте класс эмиттера событий - если вам нужна более сложная логика управления событиями, класс предоставит больше возможностей для расширения.

  3. Сочетайте keyof и дженерики - для максимальной гибкости используйте комбинацию keyof и дженериков, как показано в примерах выше.

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

Для дальнейшего изучения рекомендую обратиться к официальной документации TypeScript и изучить паттерны проектирования на основе дженериков.

Авторы
Проверено модерацией
Модерация