Типы для callback функций

Типизация callback функций в TypeScript — описание сигнатуры функции-аргумента: типов её параметров и возвращаемого значения, что позволяет компилятору проверять корректность callback при передаче и вызове.

Зачем нужно

Callbacks — функции, передаваемые как аргументы другим функциям. Без типизации callback компилятор не знает что передать и что ожидать в ответ. Типизированные callback делают API самодокументируемым и ловят ошибки несоответствия сигнатур.

Где используется

  • Асинхронные функции с callback-паттерном (Node.js стиль)
  • forEach, map, filter, reduce — типы callback выводятся из массива
  • Обработчики событий
  • Dependency injection (передача стратегий, обработчиков)
  • Promise executor: new Promise((resolve, reject) => ...)

Основной контент

Базовый синтаксис

// Тип callback в параметре функции
function doSomething(callback:  => void): void {
  callback;
}

// С параметрами
function transform(value: string, fn: (s: string) => string): string {
  return fn(value);
}

transform("hello", (s) => s.toUpperCase()); // TypeScript выводит тип s: string

Именованные типы callback

// type alias для переиспользования
type Predicate<T>    = (value: T) => boolean;
type Transform<A, B> = (input: A) => B;
type Comparator<T>   = (a: T, b: T) => number;
type Consumer<T>     = (value: T) => void;

function filter<T>(arr: T, predicate: Predicate<T>): T {
  return arr.filter(predicate);
}

function sort<T>(arr: T, compare: Comparator<T>): T {
  return [...arr].sort(compare);
}

const isEven: Predicate<number> = (n) => n % 2 === 0;
filter([1, 2, 3, 4], isEven); // [2, 4]

Callback с опциональными параметрами

// Callback может иметь меньше параметров чем ожидается — это нормально
type ClickHandler = (event: MouseEvent, index: number) => void;

// Передаём callback только с одним параметром
const handler: ClickHandler = (event) => {
  console.log(event.type); // OK — index можно игнорировать
};

// Array методы используют этот принцип
[1, 2, 3].forEach((item) => console.log(item)); // не нужен index и array
[1, 2, 3].forEach((item, index) => console.log(item, index));

Node.js callback стиль (errback)

type NodeCallback<T> = (error: Error | null, result?: T) => void;
type AsyncCallback<T> = (err: Error | null, data: T) => void;

function readConfig(path: string, callback: NodeCallback<string>): void {
  fs.readFile(path, "utf8", (err, data) => {
    if (err) {
      callback(err, undefined);
    } else {
      callback(null, data);
    }
  });
}

readConfig("./config.json", (err, data) => {
  if (err) {
    console.error(err.message);
    return;
  }
  console.log(data); // data: string | undefined
});

Async callback и Promise

// Callback возвращающий Promise
type AsyncPredicate<T> = (value: T) => Promise<boolean>;
type AsyncTransform<A, B> = (input: A) => Promise<B>;

async function filterAsync<T>(
  arr: T,
  predicate: AsyncPredicate<T>
): Promise<T> {
  const results = await Promise.all(arr.map(predicate));
  return arr.filter((_, i) => results[i]);
}

const isAdult: AsyncPredicate<User> = async (user) => {
  const data = await fetchAge(user.id);
  return data.age >= 18;
};

Callback с this-контекстом

// Если callback использует this — нужна явная аннотация
interface EventTarget {
  on(event: string, callback: (this: EventTarget, data: unknown) => void): void;
}

class Button {
  label: string;

  constructor(label: string) {
    this.label = label;
  }

  click(handler: (this: Button) => void): void {
    handler.call(this);
  }
}

Частые ошибки

  • Ожидать что callback получит строже типизированный this — без явной аннотации this в callback будет any.
  • Передать callback с большим количеством параметров чем ожидается — TypeScript разрешает меньше параметров, но не больше.
  • Путать => void и => undefined — в callback void означает «возвращаемое значение игнорируется»; можно вернуть что угодно.
  • Не типизировать callback явно при сложных HOF — без явных типов TypeScript может не вывести параметры callback.

Связанные темы

Ресурсы