TypeScript: функции как параметры со строгой типизацией — полный разбор с примерами
Привет, коллега! Если ты когда-нибудь писал на JavaScript и передавал функцию в другую функцию (например, в setTimeout или Array.map), ты уже знаком с концепцией функций высшего порядка. Но когда дело доходит до TypeScript, возникает логичный вопрос: "Можно ли строго типизировать функцию, которую я передаю как параметр?". Ответ — да, и это одна из самых мощных фишек языка.
В этой статье мы разберем, как объявлять типы функций в TypeScript, как передавать их в качестве аргументов и как не облажаться с сигнатурами. Ты узнаешь, как сделать код не только безопасным, но и самодокументируемым. Поехали!
Кстати, этот вопрос — один из самых популярных на Stack Overflow (тысячи просмотров). Так что если ты хочешь разобраться раз и навсегда — ты по адресу.
Почему это важно: строгая типизация спасает время
Представь, что ты пишешь функцию, которая принимает другую функцию (колбэк). Без типов ты можешь случайно передать функцию с неправильным количеством аргументов или не тем возвращаемым значением. TypeScript ловит такие ошибки на этапе компиляции, а не в рантайме. Это как страховка для твоего кода.
Вот пример, который не сработает без типов:
// JavaScript (без типов) — ошибка будет только в рантайме
function processData(callback) {
const result = callback(10, 20);
console.log(result);
}
// Ой, передали функцию с одним параметром
processData((x) => x * 2); // NaN — потому что второй параметр undefined
С TypeScript мы можем явно указать, какую сигнатуру ожидает колбэк:
// TypeScript — ошибка на этапе компиляции
function processData(callback: (a: number, b: number) => number): void {
const result = callback(10, 20);
console.log(result);
}
// ✅ Правильно
processData((x, y) => x + y); // 30
// ❌ Ошибка: Type '(x: number) => number' не присваивается типу '(a: number, b: number) => number'
// processData((x) => x * 2);
Круто, правда? Теперь давай разберем все способы объявления таких типов.
Способ 1: Инлайн-сигнатура (самый простой)
Самый прямолинейный способ — указать сигнатуру функции прямо в параметрах. Это удобно, когда функция-параметр используется только один раз.
// Функция, которая принимает другую функцию с двумя числами и возвращает строку
function formatNumbers(
a: number,
b: number,
formatter: (x: number, y: number) => string
): string {
return formatter(a, b);
}
// Использование
const result = formatNumbers(5, 10, (x, y) => `Сумма: ${x + y}`);
console.log(result); // "Сумма: 15"
Плюсы: быстро, наглядно.
Минусы: если сигнатура повторяется, придется дублировать код.
Способ 2: Тип-алиас для функции (переиспользуемый)
Если ты используешь одну и ту же сигнатуру в нескольких местах, лучше вынести её в тип-алиас. Это делает код чище и легче поддерживать.
// Объявляем тип функции
type Callback = (error: Error | null, data: string) => void;
// Функция, которая принимает Callback
function fetchData(url: string, callback: Callback): void {
// Имитация асинхронного запроса
setTimeout(() => {
if (url) {
callback(null, "Данные получены");
} else {
callback(new Error("URL пустой"), "");
}
}, 1000);
}
// Использование
fetchData("/api/data", (err, data) => {
if (err) {
console.error("Ошибка:", err.message);
} else {
console.log("Успех:", data);
}
});
Обрати внимание: мы использовали void как возвращаемый тип. Это значит, что функция может ничего не возвращать (или возвращать что угодно, но мы это игнорируем).
Способ 3: Интерфейс с call signature (для сложных случаев)
Иногда нужно описать функцию, у которой есть ещё и свойства (как у объекта). Для этого подходит интерфейс с call signature.
interface Comparator {
(a: number, b: number): number;
description?: string; // дополнительное свойство
}
function sortArray(arr: number[], comparator: Comparator): number[] {
return arr.sort(comparator);
}
// Создаем функцию с дополнительным свойством
const ascending: Comparator = (a, b) => a - b;
ascending.description = "По возрастанию";
const sorted = sortArray([3, 1, 4, 2], ascending);
console.log(sorted); // [1, 2, 3, 4]
console.log(ascending.description); // "По возрастанию"
Это продвинутая техника, но она пригодится, если ты пишешь библиотеки или сложные утилиты.
Способ 4: Дженерики (универсальные функции-параметры)
Если ты хочешь, чтобы функция-параметр работала с разными типами данных, используй дженерики. Это особенно полезно для функций-трансформеров.
// Функция, которая принимает массив и функцию-преобразователь
function mapArray<T, U>(arr: T[], transform: (item: T) => U): U[] {
return arr.map(transform);
}
// Пример с числами
const numbers = [1, 2, 3];
const doubled = mapArray(numbers, (n) => n * 2);
console.log(doubled); // [2, 4, 6]
// Пример со строками
const strings = ["a", "b", "c"];
const uppercased = mapArray(strings, (s) => s.toUpperCase());
console.log(uppercased); // ["A", "B", "C"]
// Пример с изменением типа
const lengths = mapArray(strings, (s) => s.length);
console.log(lengths); // [1, 1, 1]
Дженерики <T, U> гарантируют, что тип входного и выходного значения согласованы. Это мощный инструмент для функционального программирования.
Способ 5: Параметры по умолчанию и опциональные колбэки
Иногда колбэк может быть необязательным. TypeScript позволяет это легко обработать с помощью опционального параметра (?).
function loadData(url: string, callback?: (data: string) => void): void {
// Имитация загрузки
const data = "Важные данные";
if (callback) {
callback(data);
} else {
console.log("Данные загружены, но колбэк не передан");
}
}
// Без колбэка
loadData("/data"); // "Данные загружены, но колбэк не передан"
// С колбэком