Avatar ·

Возможно ли в TypeScript передавать строго типизированные функции как параметры?

Всем привет! Я недавно начал изучать TypeScript и столкнулся с проблемой при работе с функциями высшего порядка. У меня есть функция, которая принимает другую функцию в качестве аргумента, и я хочу, чтобы TypeScript проверял, что передаваемая функция имеет правильные типы параметров и возвращаемого значения. Вот пример того, что я пытаюсь сделать: ```typescript 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); // 8 // Но что если я хочу передать функцию, которая принимает строки? const badResult = applyOperation(5, 3, (a, b) => a.toString() + b.toString()); // Ошибки нет, но результат неправильный ``` В последнем случае TypeScript не ругается, потому что `a.toString() + b.toString()` возвращает строку, а ожидается число. Я хочу, чтобы компилятор строго проверял типы. Также я видел, что иногда используют такой синтаксис: ```typescript type MathOperation = (x: number, y: number) => number; function calculate(x: number, y: number, op: MathOperation): number { return op(x, y); } ``` Но мне непонятно, как правильно описывать функции, которые принимают другие функции с разными сигнатурами. Например, как сделать функцию `processArray`, которая принимает массив и функцию-обработчик, и чтобы тип обработчика соответствовал типу элементов массива? Подскажите, пожалуйста, как правильно типизировать функции-параметры в 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 даёт полную свободу для строгой типизации функций-параметров.