Как перестать использовать any для колбэков и начать жить: полный гайд по типизации функций-обработчиков в TypeScript
Вы когда-нибудь писали что-то вроде callback: any в параметрах метода? Если да — добро пожаловать в клуб. Если нет — вы либо гений, либо только начинаете свой путь в TypeScript. Шутка. Но в каждой шутке есть доля правды: по данным Stack Overflow, вопрос "How to define type for a function callback (as any function type, not universal any)" собирает тысячи просмотров. И это не случайно.
Проблема в том, что any — это как чёрная дыра: оно засасывает всю типовую безопасность вашего кода. Вы теряете автодополнение, проверку аргументов и возможность отловить баги на этапе компиляции. Но самое обидное — any в качестве типа колбэка убивает саму идею TypeScript. Давайте разберёмся, как правильно типизировать функцию callback в параметре метода, чтобы код был и безопасным, и элегантным.
Почему Function — не лучше any?
Первый порыв новичка — использовать встроенный тип Function. Мол, это же тип "любая функция", значит подойдёт. Но нет. Function — это такой же "мусорный" тип, как any. Он не даёт никакой информации о сигнатуре: сколько параметров принимает функция, что возвращает, какие типы у аргументов. Вы просто говорите компилятору: "Доверься мне, я знаю, что делаю". Спойлер: обычно не знаете.
// Плохо: никакой проверки типов
function processData(callback: Function) {
callback(42, "hello"); // Ошибка? А кто его знает
}
С таким подходом вы теряете все преимущества статической типизации. Давайте исправлять.
Шаг 1. Базовая типизация колбэка: просто и понятно
Самый простой и правильный способ — объявить тип функции прямо в параметре. Для этого используется стрелочный синтаксис (параметры) => возвращаемый_тип. Это и есть function type в TypeScript.
// Хорошо: явно задаём сигнатуру
function fetchData(url: string, onSuccess: (data: string) => void): void {
// ... какая-то логика
const result = "Данные с сервера";
onSuccess(result);
}
// Использование — с автодополнением и проверкой
fetchData("/api/users", (data) => {
console.log(data.toUpperCase()); // data — string, всё ок
});
Обратите внимание на void в возвращаемом типе. Это значит, что мы не ждём от колбэка какого-то конкретного значения. Если колбэк что-то вернёт — это проигнорируется. Если вы ожидаете результат — используйте конкретный тип.
Шаг 2. Колбэк с несколькими параметрами
Часто колбэки принимают больше одного аргумента. Например, обработчик ошибок или событие с данными. Тут всё так же просто — перечисляем все параметры через запятую.
// Тип для колбэка с ошибкой и результатом
function loadUser(
userId: number,
callback: (error: Error | null, user: { name: string; age: number } | null) => void
): void {
// ... логика загрузки
const user = { name: "Анна", age: 30 };
callback(null, user);
}
// Использование
loadUser(42, (err, user) => {
if (err) {
console.error("Ошибка:", err.message);
return;
}
if (user) {
console.log(`Привет, ${user.name}!`);
}
});
Видите, как TypeScript автоматически подсказывает типы err и user? Это и есть типизация callback функции в действии. Без any, без магии — только строгая проверка.
Шаг 3. Создаём переиспользуемые типы для колбэков
Если один и тот же тип колбэка используется в нескольких местах, не нужно дублировать его каждый раз. Вынесите его в отдельный type alias или interface. Это улучшает читаемость и упрощает поддержку.
// Объявляем тип для обработчика
type ErrorCallback = (error: Error | null, message: string) => void;
// Используем в разных функциях
function sendRequest(url: string, onComplete: ErrorCallback): void {
// ...
onComplete(null, "Успешно");
}
function saveData(payload: object, onComplete: ErrorCallback): void {
// ...
onComplete(new Error("Ошибка валидации"), "Фейл");
}
// Тип можно экспортировать и использовать в других модулях
export type { ErrorCallback };
Это особенно полезно в больших проектах, где одни и те же паттерны callback повторяются десятки раз. Плюс, если сигнатура изменится — TypeScript подсветит все места использования.
Шаг 4. Колбэки с контекстом (this)
Иногда нужно передать колбэк, который использует this. Например, методы класса. TypeScript позволяет типизировать и это. Используйте this как первый параметр в сигнатуре функции.
class DataProcessor {
private multiplier: number = 2;
// Тип колбэка с явным указанием this
process(items: number[], callback: (this: void, item: number) => number): number[] {
return items.map(callback);
}
// Альтернатива: если колбэк должен использовать this класса
processWithContext(items: number[], callback: (this: DataProcessor, item: number) => number): number[] {
return items.map(callback.bind(this));
}
}
const processor = new DataProcessor();
const result = processor.process([1, 2, 3], (item) => item * 2);
// Всё ок, this не используется
// А вот так — ошибка, если попытаться использовать this без привязки
// const badResult = processor.process([1, 2, 3], function(item) {
// return item * this.multiplier; // Ошибка: this — void
// });
Это продвинутая техника, но она спасает от неочевидных багов, когда this теряется в колбэках.
Шаг 5. Универсальный тип "любая функция" (но без any)
Что если вам действительно нужен параметр, который может принять любую функцию? Не any, а именно любую сигнатуру? Для этого используйте generic или тип (...args: any[]) => any. Это всё ещё не идеально, но гораздо лучше, чем просто any.
// Тип "любая функция" — но с возможностью уточнить
type AnyFunction = (...args: any[]) => any;
function logAndExecute(callback: AnyFunction): void {
console.log("Вызов функции...");
const result = callback();
console.log("Результат:", result);
}
// Можно передать что угодно
logAndExecute(() => 42);
logAndExecute((a: number, b: number) => a + b); // Но аргументы не проверя