Возможно ли в TypeScript передавать строго типизированные функции как параметры?
Полный ответ: типизация функций-параметров в TypeScript
Да, TypeScript полностью поддерживает строгую типизацию функций, передаваемых как параметры. Вы уже на верном пути, но давайте разберём все нюансы и продвинутые техники.
1. Базовая типизация: сигнатура функции
Самый простой способ — описать тип функции в параметре с помощью стрелочного синтаксиса:
function applyOperation(
x: number,
y: number,
operation: (a: number, b: number) => number
): number {
return operation(x, y);
}
// ✅ Работает
const sum = applyOperation(5, 3, (a, b) => a + b);
// ❌ Ошибка: Type 'string' is not assignable to type 'number'
const badResult = applyOperation(5, 3, (a, b) => a.toString() + b.toString());
В этом примере TypeScript строго проверяет, что переданная функция принимает два числа и возвращает число. Если возвращается строка — будет ошибка компиляции.
2. Использование type alias для переиспользования
Для удобства можно вынести тип в отдельный алиас:
type MathOperation = (x: number, y: number) => number;
function calculate(x: number, y: number, op: MathOperation): number {
return op(x, y);
}
// Или для более сложных случаев:
type ArrayProcessor<T> = (item: T, index: number, array: T[]) => void;
3. Дженерики (Generics) — ключ к гибкости
Для функции processArray, где тип обработчика должен соответствовать типу элементов массива, используйте дженерики:
function processArray<T>(
arr: T[],
callback: (item: T, index: number) => void
): void {
arr.forEach((item, index) => callback(item, index));
}
// Пример использования:
const numbers = [1, 2, 3];
processArray(numbers, (num, idx) => {
console.log(num.toFixed(2)); // ✅ num — number
});
const strings = ["a", "b", "c"];
processArray(strings, (str, idx) => {
console.log(str.toUpperCase()); // ✅ str — string
});
// ❌ Ошибка: Property 'toFixed' does not exist on type 'string'
processArray(strings, (str, idx) => str.toFixed(2));
4. Возвращаемое значение из callback
Можно также типизировать возвращаемое значение callback с помощью второго дженерика:
function transformArray<T, R>(
arr: T[],
transformer: (item: T, index: number) => R
): R[] {
return arr.map(transformer);
}
const doubled = transformArray([1, 2, 3], (num) => num * 2); // number[]
const strings = transformArray([1, 2, 3], (num) => num.toString()); // string[]
5. Типизация с несколькими параметрами и опциональными callback
Пример с опциональным callback и обработкой ошибок:
function fetchData<T>(
url: string,
onSuccess: (data: T) => void,
onError?: (error: Error) => void
): void {
fetch(url)
.then(res => res.json())
.then(data => onSuccess(data as T))
.catch(err => onError?.(err));
}
// Использование:
fetchData<{id: number}>(
"/api/user",
(user) => console.log(user.id), // ✅ user — {id: number}
(err) => console.error(err.message) // ✅ err — Error
);
6. Продвинутая техника: Conditional Types для фильтрации
Можно сделать так, чтобы callback принимал только определённые типы:
type FilterCallback<T, U extends T> = (item: T) => item is U;
function filterByType<T, U extends T>(
arr: T[],
predicate: FilterCallback<T, U>
): U[] {
return arr.filter(predicate);
}
const mixed: (string | number)[] = [1, "hello", 2, "world"];
const numbers = filterByType(mixed, (item): item is number => typeof item === "number");
// numbers: number[] — TypeScript знает, что остались только числа
Итог
- Базовая типизация: используйте
(param1: Type1, param2: Type2) => ReturnTypeпрямо в параметре - Для переиспользования: выносите в
typeилиinterface - Для гибкости: применяйте дженерики
<T>и<T, R> - Для сложной логики: используйте условные типы и type guards
TypeScript — это мощный инструмент, который позволяет описать практически любую сигнатуру функции. Главное — начать с простого и постепенно осваивать продвинутые возможности.
<<Альтернативный подход: использование интерфейсов и перегрузок
Помимо type alias, можно использовать интерфейсы для описания функций-параметров, особенно если вам нужно добавить дополнительные свойства или перегрузки:
interface MathOperation {
(x: number, y: number): number;
description?: string; // Дополнительное свойство
}
function applyWithDesc(op: MathOperation, x: number, y: number): number {
if (op.description) {
console.log(`Using: ${op.description}`);
}
return op(x, y);
}
const add: MathOperation = (a, b) => a + b;
add.description = "Addition";
applyWithDesc(add, 5, 3); // 8
Перегрузки функций
Если ваша функция-параметр может иметь разные сигнатуры в зависимости от контекста, используйте перегрузки:
function handleEvent(event: "click", handler: (e: MouseEvent) => void): void;
function handleEvent(event: "keydown", handler: (e: KeyboardEvent) => void): void;
function handleEvent(event: string, handler: (e: Event) => void): void {
document.addEventListener(event, handler);
}
// ✅ Корректно
handleEvent("click", (e) => console.log(e.clientX)); // e — MouseEvent
handleEvent("keydown", (e) => console.log(e.key)); // e — KeyboardEvent
// ❌ Ошибка: e — KeyboardEvent, но clientX есть только у MouseEvent
handleEvent("keydown", (e) => console.log(e.clientX));
Когда что использовать?
- type alias — для простых функций без дополнительных свойств
- interface — если нужно добавить свойства к функции или использовать наследование
- перегрузки — когда функция-параметр может иметь разные сигнатуры в зависимости от первого аргумента
Оба подхода валидны, и выбор зависит от конкретной задачи. Главное — TypeScript даёт полную свободу для строгой типизации функций-параметров.