Avatar ·

Как правильно типизировать колбэк-функцию в параметре метода TypeScript?

Всем привет! Я только начинаю разбираться с TypeScript и столкнулся с проблемой. В моём классе есть метод, который принимает колбэк-функцию. Я хочу типизировать параметр так, чтобы можно было передать любую функцию (с любым количеством аргументов и возвращаемым значением), но при этом не использовать `any`, чтобы сохранить проверку типов. Вот что я пробую: ```typescript class EventBus { on(eventName: string, callback: any) { // так работает, но это небезопасно // ... } } const bus = new EventBus(); bus.on('click', (data: number) => console.log(data)); // ошибки не будет, даже если передать строку ``` Я понимаю, что `any` отключает проверку типов полностью и это плохая практика. Пытался использовать `Function`, но он тоже слишком широкий и не позволяет нормально работать с аргументами. В документации видел `(...args: any[]) => any`, но мне кажется это костылём. Есть ли более элегантный способ? Как правильно объявить тип "любая функция" в TypeScript, чтобы: 1. Можно было передать любую функцию (с любыми параметрами и возвращаемым значением) 2. Сохранить возможность вызова внутри метода с правильной типизацией 3. Не использовать `any` в параметрах Буду благодарен за примеры кода и объяснение! <<>>

Правильная типизация колбэков с помощью дженериков

Проблема в том, что 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 знает типы