Avatar ·

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

Всем привет! Я начал изучать TypeScript и столкнулся с проблемой при типизации колбэков. У меня есть метод, который принимает функцию обратного вызова, и я хочу строго указать её тип, но не знаю как это сделать правильно. Вот пример. У меня есть класс `DataProcessor`, который обрабатывает данные и вызывает колбэк после завершения: ```typescript class DataProcessor { process(data: number[], callback: ???) { const result = data.map(x => x * 2); callback(result); } } ``` Проблема в том, что я не знаю, что написать вместо `???`. Если я пишу `callback: any`, то TypeScript перестаёт проверять типы и я теряю все преимущества статической типизации. Если пишу `callback: Function`, то тоже не получаю автодополнения и проверки аргументов. Я хочу, чтобы колбэк мог быть любой функцией, но при этом TypeScript проверял, что она принимает правильное количество аргументов и возвращает что-то (или ничего). Как правильно объявить тип для такого колбэка? Нужно ли описывать его сигнатуру полностью или есть какой-то универсальный способ? Подскажите, пожалуйста, как это делается в реальных проектах. Заранее спасибо! <<>>

Основной подход: явное описание сигнатуры функции

В TypeScript лучший способ типизировать колбэк — это явно описать его сигнатуру, используя стрелочную функцию в качестве типа. Это даёт полную проверку типов и автодополнение в IDE.

Для вашего примера с DataProcessor правильное решение будет таким:

class DataProcessor {
  process(data: number[], callback: (result: number[]) => void) {
    const result = data.map(x => x * 2);
    callback(result);
  }
}

// Использование:
const processor = new DataProcessor();
processor.process([1, 2, 3], (result) => {
  console.log(result); // result: number[]
});

Здесь (result: number[]) => void означает, что колбэк принимает один аргумент типа number[] и ничего не возвращает (void). TypeScript будет проверять:

  • Что колбэк вызывается только с одним аргументом типа number[]
  • Что колбэк не возвращает значение (или возвращает undefined)
  • Что при вызове метода передаётся функция с правильной сигнатурой

Использование type alias для переиспользования

Если один и тот же тип колбэка используется в нескольких местах, лучше вынести его в отдельный тип:

type DataCallback = (result: number[]) => void;

class DataProcessor {
  process(data: number[], callback: DataCallback) {
    // ...
  }

  processAsync(data: number[], callback: DataCallback) {
    // ...
  }
}

Типизация с возвращаемым значением

Если колбэк должен что-то возвращать, укажите тип возврата:

type TransformCallback = (item: number) => string;

function transformArray(arr: number[], callback: TransformCallback): string[] {
  return arr.map(callback);
}

const result = transformArray([1, 2, 3], (num) => `Number: ${num}`);
// result: string[]

Дополнительные аргументы и опциональность

Колбэк может принимать несколько аргументов или опциональные параметры:

type ErrorFirstCallback = (error: Error | null, data?: string) => void;

function fetchData(callback: ErrorFirstCallback) {
  try {
    const data = "some data";
    callback(null, data);
  } catch (err) {
    callback(err as Error);
  }
}

Важные моменты

  • Не используйте any или Function — они отключают проверку типов
  • Используйте void если колбэк не должен возвращать значение — это позволяет передавать функции, которые возвращают что-то, но это значение будет проигнорировано
  • Для опциональных колбэков используйте callback?: (arg: Type) => void и проверяйте наличие перед вызовом

Этот подход широко используется в библиотеках TypeScript (например, Array.prototype.map, Promise.then) и обеспечивает максимальную безопасность типов.

<<>>

Альтернатива: использование обобщённых типов (generics)

Если ваш колбэк может работать с разными типами данных, можно сделать метод обобщённым:

class DataProcessor {
  process<T>(data: T[], callback: (item: T, index: number) => void): void {
    data.forEach((item, index) => callback(item, index));
  }
}

// Использование с числами:
const processor = new DataProcessor();
processor.process([1, 2, 3], (item, index) => {
  console.log(item.toFixed(2)); // item: number
});

// Использование со строками:
processor.process(['a', 'b', 'c'], (item, index) => {
  console.log(item.toUpperCase()); // item: string
});

Этот подход полезен, когда ваш метод должен быть универсальным и работать с разными типами данных, сохраняя при этом строгую типизацию. Компилятор TypeScript автоматически выведет тип T из переданного массива.

Также можно комбинировать обобщения с несколькими параметрами:

function mapWithCallback<T, R>(
  arr: T[], 
  callback: (item: T) => R
): R[] {
  return arr.map(callback);
}

const numbers = mapWithCallback([1, 2, 3], (x) => x * 2); // number[]
const strings = mapWithCallback([1, 2, 3], (x) => String(x)); // string[]

Это даёт максимальную гибкость без потери безопасности типов.