Типизация callback-функций
Функции как параметры — как типизировать callback-и, использоватьgenerics для типобезопасности и создавать гибкие API.
🎯 Что такое callback-функции?
Callback-функция — это функция, которую передают друг другу как аргумент, чтобы её позже вызвали. Это один из самых мощных паттернов в JavaScript/TypeScript, лежащий в основе асинхронного программирования, обработки событий и функционального стиля.
Вы уже встречались с callback-ами countless раз:
// Массивы
[1, 2, 3].map(n => n * 2); // n => n * 2 — callback
[1, 2, 3].filter(n => n > 1); // n => n > 1 — callback
[1, 2, 3].forEach(n => console.log(n)); // n => console.log(n) — callback
// События
button.addEventListener("click", handleClick); // handleClick — callback
// Асинхронность
setTimeout(() => console.log("Привет"), 1000); // () => console.log(...) — callback
// Промисы
fetch("/api").then(response => response.json()); // response => ... — callback
Без типизации callback-функций TypeScript не может проверить, что переданная функция имеет правильную сигнатуру. Это может привести к ошибкамво время выполнения.
// ❌ Без типизации — TypeScript не может проверить
function execute(callback: any) {
callback("hello", 42); // Что если callback ожидает другие аргументы?
}
execute((name: string) => console.log(name)); // Ошибкаво время выполнения!
💡 Аналогия: Представьте, что вы отправляете письмо с открыткой для ответа. Если не указать формат ответа (какой текст, сколько слов), получатель может ответить как угодно. Типизация callback-ов — это «инструкция по формату ответа».
📝 Базовая типизация callback-функций
Типизация callback — это описание типа функции прямо в сигнатуре параметра.
Inline-типизация
// Простой callback — принимает строку, ничего не возвращает
function greetUser(name: string, callback: (name: string) => void): void {
const greeting = "Привет, " + name + "!";
callback(greeting);
}
// Вызов
greetUser("Анна", (msg) => {
console.log(msg); // "Привет, Анна!"
});
// Callback с возвращаемым значением
function processArray(
items: number[],
processor: (item: number) => number
): number[] {
return items.map(processor);
}
const doubled = processArray([1, 2, 3], (n) => n * 2);
console.log(doubled); // [2, 4, 6]
Тип callback через type alias
// Определяем тип callback-а отдельно
type Callback = (message: string) => void;
type ErrorHandler = (error: Error) => void;
type DataProcessor = (data: T) => T;
type Predicate = (item: T) => boolean;
// Используем в функциях
function log(message: string, callback: Callback): void {
callback(message);
}
function fetchData(url: string, onError: ErrorHandler): void {
try {
// загрузка...
} catch (error) {
onError(error as Error);
}
}
function transform(
data: T[],
processor: DataProcessor
): T[] {
return data.map(processor);
}
Тип callback через interface
// Интерфейс для функции (callable interface)
interface Formatter {
(input: string): string;
}
interface Comparator {
(a: T, b: T): number;
}
interface AsyncLoader {
(url: string): Promise;
}
// Использование
function sort(array: T[], comparator: Comparator): T[] {
return [...array].sort(comparator);
}
const numbers = [3, 1, 4, 1, 5, 9];
sort(numbers, (a, b) => a - b); // [1, 1, 3, 4, 5, 9]
sort(numbers, (a, b) => b - a); // [9, 5, 4, 3, 1, 1]
🔧 Callback сgenerics (Generics)
Дженерик (generics) позволяют создавать типобезопасные callback-и, которые работают с любыми типами данных.
map — маппинг значений
// Типобезопасный map
function map(array: T[], transform: (item: T) => R): R[] {
return array.map(transform);
}
// Использование
const numbers = [1, 2, 3, 4, 5];
// Число → строка
const strings = map(numbers, (n) => n.toString());
// strings: string[]
// Число → boolean
const isEvens = map(numbers, (n) => n % 2 === 0);
// isEvens: boolean[]
// Строка → объект
const lengths = map(["hello", "world"], (s) => ({ text: s, length: s.length }));
// lengths: { text: string; length: number }[]
filter — фильтрация
// Типобезопасный filter
function filter(array: T[], predicate: (item: T) => boolean): T[] {
return array.filter(predicate);
}
// Использование
const users = [
{ name: "Анна", age: 25, active: true },
{ name: "Борис", age: 30, active: false },
{ name: "Вика", age: 22, active: true }
];
// Фильтрация по условию
const activeUsers = filter(users, (u) => u.active);
// activeUsers: typeof users
// Фильтрация по age
const adults = filter(users, (u) => u.age >= 18);
// adults: typeof users
reduce — редуция
// Типобезопасный reduce
function reduce(
array: T[],
reducer: (accumulator: R, item: T) => R,
initialValue: R
): R {
return array.reduce(reducer, initialValue);
}
// Использование
const numbers = [1, 2, 3, 4, 5];
// Сумма
const sum = reduce(numbers, (acc, n) => acc + n, 0);
// sum: number
// Объект-счётчик
const wordCount = reduce(
["hello", "world", "hello", "typescript"],
(acc, word) => {
acc[word] = (acc[word] || 0) + 1;
return acc;
},
{} as Record
);
// wordCount: Record
find — поиск
// Типобезопасный find
function find(
array: T[],
predicate: (item: T) => boolean
): T | undefined {
return array.find(predicate);
}
// Использование
const numbers = [1, 2, 3, 4, 5];
const found = find(numbers, (n) => n > 3);
// found: number | undefined
if (found !== undefined) {
console.log(found); // 4
}
🔗 Callback с несколькими параметрами
Callback-функции могут принимать несколько параметров. Давайте разберём разные сценарии.
Callback с индексом
// forEach с индексом
function forEach(
array: T[],
callback: (item: T, index: number) => void
): void {
for (let i = 0; i < array.length; i++) {
callback(array[i], i);
}
}
// Использование
forEach(["а", "б", "в"], (letter, index) => {
console.log(`${index + 1}. ${letter}`);
});
// 1. а
// 2. б
// 3. в
Callback с несколькими аргументами
// Обработчик событий с типизацией
type EventHandler = (event: T, timestamp: number) => void;
function onEvent(
eventName: string,
handler: EventHandler
): void {
console.log(`Регистрация обработчика для ${eventName}`);
// Предположим, что event вызывается позже
// handler(event, Date.now());
}
// Использование
onEvent<{ type: string; value: number }>("data", (event, timestamp) => {
console.log(`${timestamp}: ${event.type} = ${event.value}`);
});
// Callback с тремя параметрами
function sortWithKey(
array: T[],
keyFn: (item: T) => K,
order: "asc" | "desc" = "asc"
): T[] {
return [...array].sort((a, b) => {
const keyA = keyFn(a);
const keyB = keyFn(b);
const comparison = keyA < keyB ? -1 : keyA > keyB ? 1 : 0;
return order === "asc" ? comparison : -comparison;
});
}
const users = [
{ name: "Вика", age: 22 },
{ name: "Анна", age: 25 },
{ name: "Борис", age: 30 }
];
sortWithKey(users, (u) => u.age, "desc");
// [{ name: "Борис", age: 30 }, { name: "Анна", age: 25 }, { name: "Вика", age: 22 }]
Callback с переменным числом аргументов
// Функция, принимающая callback с rest-параметрами
function processAll(
items: T[],
processor: (...args: T[]) => void
): void {
processor(...items);
}
// Использование
processAll([1, 2, 3], (a, b, c) => {
console.log(`Сумма: ${a + b + c}`);
});
// Callback с rest-параметрами
function logAll(
level: string,
...messages: unknown[]
): void {
console.log(`[${level}]`, ...messages);
}
// Передаём как callback
const logger: (level: string, ...messages: unknown[]) => void = logAll;
logger("info", "Сервер", "запущен", "на", "порту", 3000);
🔖 Callback и контекст this
Одна из самых частых проблем с callback-ами — потеря контекста this. TypeScript помогает избежать этой проблемы через типизацию.
Проблема потери this
class EventEmitter {
private listeners: Record = {};
// ❌ Проблема: this в callback-е не EventEmitter
on(event: string, callback: Function): void {
if (!this.listeners[event]) {
this.listeners[event] = [];
}
this.listeners[event].push(callback);
}
emit(event: string): void {
const callbacks = this.listeners[event] || [];
callbacks.forEach(cb => cb()); // this = undefined (или window)
}
}
Решение 1: Стрелочные функции
class EventEmitter {
private listeners: Record = {};
// ✅ Стрелочная функция сохраняет this
on(event: string, callback: () => void): void {
if (!this.listeners[event]) {
this.listeners[event] = [];
}
this.listeners[event].push(callback);
}
emit(event: string): void {
const callbacks = this.listeners[event] || [];
callbacks.forEach(cb => cb()); // cb — стрелочная, this сохраняется
}
}
Решение 2: Типизация this в callback
// Типизируем this для callback-а
type ThisCallback = (this: T) => void;
class Logger {
private prefix: string;
constructor(prefix: string) {
this.prefix = prefix;
}
// Принимаем callback с типизированным this
log(message: string, callback: ThisCallback): void {
console.log(`${this.prefix}: ${message}`);
callback.call(this); // Привязываем this к Logger
}
}
const logger = new Logger("INFO");
logger.log("Загрузка", function() {
// this: Logger — работает!
console.log(this.prefix); // "INFO"
});
Решение 3: Стрелочные функции в интерфейсах
// Интерфейс с типом this
interface Handler {
(this: TContext, event: Event): void;
}
class Button {
private handlers: Handler[] = [];
onClick(handler: Handler): void {
this.handlers.push(handler);
}
click(): void {
this.handlers.forEach(handler => {
handler.call(this, new Event("click"));
});
}
}
const button = new Button();
button.onClick(function() {
// this: Button — благодаря типизации
console.log("Кнопка нажата!");
});
⏳ Промисы и async callback-функции
В современном TypeScript callback-и всё чаще заменяются промисами и async/await. Но типизация промисов — это тоже типизация callback-ов.
Типизация промисов
// Функция, возвращающая промис
function fetchUser(id: string): Promise<{ id: string; name: string }> {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (id === "0") {
reject(new Error("Неверный ID"));
} else {
resolve({ id, name: "Пользователь " + id });
}
}, 1000);
});
}
// Callback с промисом
function loadAndProcess(
id: string,
onSuccess: (user: { id: string; name: string }) => void,
onError: (error: Error) => void
): void {
fetchUser(id)
.then(onSuccess)
.catch(onError);
}
// Использование
loadAndProcess(
"123",
(user) => console.log("Пользователь:", user.name),
(error) => console.error("Ошибка:", error.message)
);
Async/await вместо callback
// Вместо callback — промисы + async/await
async function loadUser(id: string): Promise<{ id: string; name: string }> {
const response = await fetch(`/api/users/${id}`);
if (!response.ok) {
throw new Error("Пользователь не найден");
}
return response.json();
}
// Использование
async function main() {
try {
const user = await loadUser("123");
console.log(user.name);
} catch (error) {
console.error(error);
}
}
// Функция с callback-ом → промис
function withTimeout(
promise: Promise,
timeout: number
): Promise {
return Promise.race([
promise,
new Promise((_, reject) =>
setTimeout(() => reject(new Error("Timeout")), timeout)
)
]);
}
// Использование
const user = await withTimeout(fetchUser("123"), 5000);
🏗️ Паттерны с callback-ами
Паттерн 1: Visitor
// Паттерн Visitor: обход дерева с callback-ом
interface TreeNode {
value: number;
left?: TreeNode;
right?: TreeNode;
}
type Visitor = (node: TreeNode) => void;
function traverse(node: TreeNode | undefined, visitor: Visitor): void {
if (!node) return;
visitor(node);
traverse(node.left, visitor);
traverse(node.right, visitor);
}
// Использование
const tree: TreeNode = {
value: 1,
left: { value: 2, left: { value: 4 }, right: { value: 5 } },
right: { value: 3, left: { value: 6 }, right: { value: 7 } }
};
const values: number[] = [];
traverse(tree, (node) => values.push(node.value));
console.log(values); // [1, 2, 4, 5, 3, 6, 7]
Паттерн 2: Strategy
// Паттерн Strategy: разные стратегии сортировки
type SortStrategy = (a: T, b: T) => number;
function sort(array: T[], strategy: SortStrategy): T[] {
return [...array].sort(strategy);
}
// Стратегии
const byAge: SortStrategy<{ name: string; age: number }> = (a, b) => a.age - b.age;
const byName: SortStrategy<{ name: string; age: number }> = (a, b) => a.name.localeCompare(b.name);
const byAgeDesc: SortStrategy<{ name: string; age: number }> = (a, b) => b.age - a.age;
// Использование
const people = [
{ name: "Вика", age: 22 },
{ name: "Анна", age: 25 },
{ name: "Борис", age: 30 }
];
sort(people, byAge); // [{ name: "Вика", age: 22 }, { name: "Анна", age: 25 }, { name: "Борис", age: 30 }]
sort(people, byName); // [{ name: "Анна", age: 25 }, { name: "Борис", age: 30 }, { name: "Вика", age: 22 }]
sort(people, byAgeDesc); // [{ name: "Борис", age: 30 }, { name: "Анна", age: 25 }, { name: "Вика", age: 22 }]
Паттерн 3: Middleware
// Паттерн Middleware: цепочка обработки
type Middleware = (data: T, next: () => void) => void;
function createPipeline(...middlewares: Middleware[]): (data: T) => void {
return (data: T) => {
let index = 0;
function next(): void {
if (index < middlewares.length) {
const middleware = middlewares[index++];
middleware(data, next);
}
}
next();
};
}
// Использование
const log: Middleware = (data, next) => {
console.log("Лог:", data);
next();
};
const validate: Middleware = (data, next) => {
if (data.length > 0) {
next();
} else {
console.log("Ошибка: пустые данные");
}
};
const save: Middleware = (data, next) => {
console.log("Сохранение:", data);
next();
};
const pipeline = createPipeline(log, validate, save);
pipeline("Hello!"); // Лог: Hello! → Сохранение: Hello!
pipeline(""); // Лог: → Ошибка: пустые данные
Паттерн 4: Event Emitter
// Типизированный EventEmitter
type EventMap = Record;
class TypedEmitter {
private listeners: { [K in keyof Events]?: Array<(...args: Events[K]) => void> } = {};
on(
event: K,
listener: (...args: Events[K]) => void
): void {
if (!this.listeners[event]) {
this.listeners[event] = [];
}
this.listeners[event]!.push(listener);
}
emit(event: K, ...args: Events[K]): void {
const callbacks = this.listeners[event] || [];
for (const callback of callbacks) {
callback(...args);
}
}
off(
event: K,
listener: (...args: Events[K]) => void
): void {
const callbacks = this.listeners[event] || [];
this.listeners[event] = callbacks.filter(cb => cb !== listener) as any;
}
}
// Использование
interface AppEvents {
login: [userId: string];
logout: [userId: string];
error: [code: number, message: string];
}
const emitter = new TypedEmitter();
emitter.on("login", (userId) => {
console.log("Вошёл:", userId);
});
emitter.on("error", (code, message) => {
console.error(`Ошибка ${code}: ${message}`);
});
emitter.emit("login", "user123"); // ✅ OK
emitter.emit("error", 404, "Не найдено"); // ✅ OK
// emitter.emit("error", "bad"); // ❌ Ошибка типа
⚠️ Callback и обработка ошибок
Типизация callback-ов помогает правильно обрабатывать ошибки. Рассмотрим паттерны.
Node.js-style callback (error-first)
// Node.js стиль: первый аргумент — ошибка
type NodeCallback = (error: Error | null, result?: T) => void;
function readFile(
path: string,
callback: NodeCallback
): void {
try {
const content = "Содержимое файла"; // имитация
callback(null, content);
} catch (error) {
callback(error as Error);
}
}
// Использование
readFile("/etc/config", (error, result) => {
if (error) {
console.error("Ошибка:", error.message);
return;
}
// result: string | undefined
console.log(result); // "Содержимое файла"
});
Promise-style callback
// Promise стиль: resolve/reject
function fetchData(
url: string,
resolve: (data: T) => void,
reject: (error: Error) => void
): void {
fetch(url)
.then(res => res.json())
.then(data => resolve(data as T))
.catch(err => reject(err));
}
// Использование
fetchData<{ users: string[] }>("/api/users",
(data) => console.log(data.users),
(error) => console.error(error.message)
);
Result callback (не-исключения)
// Result тип: Success | Failure — без исключений
type Result =
| { ok: true; value: T }
| { ok: false; error: string };
type ResultCallback = (result: Result) => void;
function divide(
a: number,
b: number,
callback: ResultCallback
): void {
if (b === 0) {
callback({ ok: false, error: "Деление на ноль" });
} else {
callback({ ok: true, value: a / b });
}
}
// Использование — нет try/catch
divide(10, 2, (result) => {
if (result.ok) {
console.log("Результат:", result.value); // 5
} else {
console.log("Ошибка:", result.error);
}
});
divide(10, 0, (result) => {
if (result.ok) {
console.log("Результат:", result.value);
} else {
console.log("Ошибка:", result.error); // "Деление на ноль"
}
});
❌ Частые ошибки
Ошибка 1: Нетипизированный callback
// ❌ Плохо — callback без типизации
function process(data: unknown, callback: Function) {
callback(data); // Что если callback ожидает другой тип?
}
// ✅ Хорошо — явная типизация callback
function process(data: T, callback: (data: T) => void) {
callback(data); // TypeScript знает тип data
}
Ошибка 2: Несовпадение типов callback
// ❌ Ошибка типа
function execute(callback: (x: number) => string) {
return callback(42);
}
execute((x) => x * 2); // Ошибка: number не assignable к string
// ✅ Правильно
execute((x) => x.toString());
execute((x) => `Число: ${x}`);
Ошибка 3: Забыли return в callback
// ❌ Callback ничего не возвращает
const numbers = [1, 2, 3];
const doubled = numbers.map((n) => {
n * 2; // Забыли return!
});
// doubled: [undefined, undefined, undefined]
// ✅ Правильно — неявный или явный return
const doubled1 = numbers.map((n) => n * 2);
const doubled2 = numbers.map((n) => { return n * 2; });
Ошибка 4: Потеря this в callback
class Timer {
seconds = 0;
// ❌ this потеряется
start(): void {
setInterval(function() {
this.seconds++; // this ≠ Timer
}, 1000);
}
// ✅ Исправление — стрелочная функция
startFixed(): void {
setInterval(() => {
this.seconds++; // this = Timer
}, 1000);
}
// ✅ Альтернатива — bind
startBound(): void {
setInterval(function() {
this.seconds++; // this = Timer (благодаря bind)
}.bind(this), 1000);
}
}
📋 Итоги урока
- Callback-функция — функция, переданная как аргумент другой функции
- Типизация: описывайте тип callback inline или через type alias
- generics (Generics) — создают типобезопасные callback-и для любых типов
- this: стрелочные функции сохраняют контекст, обычные — нет
- Промисы: заменяют callback-и в асинхронном коде
- Паттерны: Visitor, Strategy, Middleware, Event Emitter — всё через callback-и
- Ошибки: типизируйте callback, проверяйте типы, не теряйте this
✅ Ключевое правило: Типизация callback-ов — это контракт между вызывающим и вызываемым кодом. Чётко описывайте, какие аргументы ожидает callback и что он возвращает. Это делает код безопасным и самодокументированным.
🎉 Поздравляем! Вы завершили Модуль 4: Функции. Теперь вы умеете типизировать параметры и возвращаемые значения, работать с необязательными параметрами, rest-параметрами, перегрузкой функций, стрелочными функциями и callback-ами. Это основа для создания типобезопасных и гибких API.
Проверяем понимание
6 вопросов