Типизация колбэков в TypeScript: как задать тип для функции-обработчика

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Как перестать использовать 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); // Но аргументы не проверя                

Блоги

Рекомендаций по книгам