Как убрать второй аргумент дженерика в 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
Я хотел бы добиться того же результата, но без использования второго аргумента в дженерике, чтобы можно было вызывать функцию так:
const emitTest1 = useEmitter('cell:hover');
const emitTest2 = useEmitter("test2");
Я пробовал следующий подход, но он создает зависимость от конкретного TableEventMap, а я хотел бы оставить общий тип:
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 для автоматического определения типа события на основе переданной строки:
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 в качестве первого аргумента. Для более элегантного решения можно использовать фабричную функцию:
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. Использование класса с дженериком
Вы можете создать класс эмиттера событий, который будет автоматически выводить типы:
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:
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'
Работа с ограниченными типами событий
Если вы хотите создать более строгий тип для событий, можно использовать литеральные типы:
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 для событий
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: Классический эмиттер событий
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, наиболее рекомендуемым подходом является использование фабричной функции, так как он обеспечивает наилучший баланс между типобезопасностью и удобством использования.
Источники
- TypeScript Documentation - Generics and Type Inference
- r/TypeScript - Type inference solution
- Type-Safe Event Emitter Implementation
- Stack Overflow - Event listener React hook with type inference
- Stack Overflow - Preserve type inference when mapping generic Record keys
Заключение
Для решения вашей задачи по созданию типобезопасного эмиттера событий без второго дженерика параметра рекомендую следующие подходы:
-
Используйте фабричную функцию - это самый чистый и типобезопасный способ, который позволит вам вызывать
useEmitter('cell:hover')без указания дженериков. -
Реализуйте класс эмиттера событий - если вам нужна более сложная логика управления событиями, класс предоставит больше возможностей для расширения.
-
Сочетайте keyof и дженерики - для максимальной гибкости используйте комбинацию
keyofи дженериков, как показано в примерах выше.
Все эти подходы обеспечивают полную типобезопасность при сохранении удобства использования. Выбор конкретного подхода зависит от сложности вашего приложения и требований к архитектуре.
Для дальнейшего изучения рекомендую обратиться к официальной документации TypeScript и изучить паттерны проектирования на основе дженериков.