Как правильно типизировать колбэк-функцию в параметре метода TypeScript?
Правильная типизация колбэков с помощью дженериков
Проблема в том, что any полностью отключает проверку типов, а Function не даёт информации о сигнатуре. Лучшее решение — использовать дженерики (generics) с ограничением на тип функции.
Вот основной подход для типизации "любой функции" без потери типобезопасности:
// Базовый тип для любой функции
type AnyFunction = (...args: any[]) => any;
class EventBus {
on(eventName: string, callback: AnyFunction) {
// Внутри метода можно вызвать колбэк с любыми аргументами
callback();
callback(1, 'string');
}
}
const bus = new EventBus();
// Это сработает
bus.on('click', (data: number) => console.log(data));
// И это тоже
bus.on('submit', (name: string, age: number) => {});
Почему это лучше, чем any?
AnyFunctionявно указывает, что параметр — функция- TypeScript проверяет, что переданное значение действительно является функцией
- Внутри метода можно вызывать колбэк, хотя типы аргументов будут потеряны
- Нельзя случайно передать число или объект вместо функции
Более точная типизация с дженериками
Если нужно сохранить информацию о конкретных типах аргументов колбэка, используйте дженерики:
class EventBus {
on<T extends (...args: any[]) => any>(
eventName: string,
callback: T
): void {
// Вызов с сохранением типа
callback();
}
}
// TypeScript выведет типы автоматически
bus.on('click', (data: number) => data.toString());
// Параметр data будет number
// Ошибка при несоответствии типов
bus.on('click', (data: string) => {}); // Ошибка!
Продвинутый вариант: конкретный колбэк с параметрами
Часто требуется, чтобы колбэк имел определённую сигнатуру. Например, обработчик события:
// Определяем конкретный тип колбэка
type EventCallback<T = any> = (event: T) => void;
interface ClickEvent {
x: number;
y: number;
target: HTMLElement;
}
class EventBus {
private handlers: Map<string, EventCallback[]> = new Map();
on<T>(eventName: string, callback: EventCallback<T>): void {
const handlers = this.handlers.get(eventName) || [];
handlers.push(callback as EventCallback);
this.handlers.set(eventName, handlers);
}
emit<T>(eventName: string, data: T): void {
const handlers = this.handlers.get(eventName);
handlers?.forEach(cb => cb(data));
}
}
// Использование с правильной типизацией
const bus = new EventBus();
bus.on<ClickEvent>('click', (event) => {
console.log(event.x, event.y); // TypeScript знает типы
});
Сравнение подходов
| Подход | Безопасность | Гибкость |
|---|---|---|
any |
Нет | Полная |
Function |
Минимальная | Средняя |
AnyFunction |
Средняя | Высокая |
| Дженерики | Высокая | Максимальная |
Рекомендация: Используйте дженерики, когда нужно сохранить типы аргументов колбэка. Если достаточно просто "любой функции" — AnyFunction будет хорошим компромиссом между безопасностью и удобством.
Альтернативный подход: перегрузка функций
Если вам нужно поддерживать колбэки с разным количеством аргументов, можно использовать перегрузку функций (function overloads):
class EventBus {
// Перегрузки
on(eventName: string, callback: () => void): void;
on(eventName: string, callback: (data: number) => void): void;
on(eventName: string, callback: (data: string) => void): void;
// Реализация
on(eventName: string, callback: (...args: any[]) => void): void {
// Реализация
}
}
// Использование
bus.on('noData', () => {});
bus.on('withNumber', (n: number) => {});
bus.on('withString', (s: string) => {});
// Ошибка при передаче неподдерживаемой сигнатуры
bus.on('invalid', (a: number, b: number) => {}); // Ошибка!
Когда это полезно?
- Когда у вас конечный набор возможных сигнатур колбэков
- Когда нужно строго контролировать, какие функции можно передавать
- В публичных API библиотек для лучшей документации
Недостаток: При большом количестве вариантов код становится громоздким. Для динамических случаев лучше использовать дженерики или AnyFunction.
Ещё один простой вариант — использовать Parameters<T> и ReturnType<T> для работы с типами функций:
function wrapFunction<T extends (...args: any[]) => any>(fn: T): T {
return ((...args: Parameters<T>): ReturnType<T> => {
console.log('Called with:', args);
return fn(...args);
}) as T;
}
const wrapped = wrapFunction((x: number, y: string) => x + y);
wrapped(1, 'test'); // TypeScript знает типы
Ответов пока нет.
Войдите чтобы оставить ответ
Блоги