Меньше кода, больше пользы: разбираемся со стрелочными функциями и дженериками в TypeScript
Вы когда-нибудь писали небольшую утилиту на TypeScript, которая должна была работать и с числами, и со строками, и с массивами? И тут же сталкивались с проблемой: как сделать так, чтобы тип данных был гибким, но при этом TypeScript не ругался? Ответ — дженерики (generics, или обобщённые типы). А если вы любите лаконичный синтаксис, то наверняка используете стрелочные функции.
Но вот незадача: на Stack Overflow вопрос «What is the syntax for Typescript arrow functions with generics?» набрал тысячи просмотров. Оказывается, даже опытные разработчики иногда путаются в этом, казалось бы, простом сочетании. В этой статье мы разберёмся, как правильно писать TypeScript arrow functions с дженериками, рассмотрим частые ошибки и покажем, как сделать ваш код чище и безопаснее.
Готовы? Поехали!
Что такое дженерики в стрелочных функциях?
Для начала — краткий ликбез. Дженерики позволяют создавать компоненты (функции, классы, интерфейсы), которые работают с разными типами, а не с одним конкретным. Вместо того чтобы писать отдельные функции для чисел и строк, вы пишете одну — с дженериком.
Стрелочные функции (arrow functions) — это компактный синтаксис функций в JavaScript и TypeScript. Когда вы объединяете их с дженериками, получаете мощный инструмент для создания переиспользуемых утилит.
// Обычная функция с дженериком
function identity<T>(arg: T): T {
return arg;
}
// Стрелочная функция с дженериком — как это выглядит?
const arrowIdentity = <T>(arg: T): T => arg;
Всё просто? Да, но есть нюансы, которые мы разберём ниже.
Базовый синтаксис: как правильно объявить TypeScript arrow function с generics
Самый простой и распространённый способ — указать тип-параметр (дженерик) перед круглыми скобками с аргументами:
const firstElement = <T>(arr: T[]): T | undefined => {
return arr[0];
};
// Использование
const num = firstElement([1, 2, 3]); // type: number | undefined
const str = firstElement(['a', 'b']); // type: string | undefined
Здесь <T> — это объявление дженерика. TypeScript автоматически выведет тип T на основе переданного аргумента. Если вы явно хотите указать тип, это тоже работает:
const num = firstElement<number>([1, 2, 3]); // Явно указываем тип
Важно: В JSX (React) этот синтаксис может конфликтовать с разметкой. Для таких случаев есть обходные пути (о них ниже).
Несколько дженериков в одной стрелочной функции
Вы можете использовать несколько типов-параметров:
const pair = <T, U>(first: T, second: U): [T, U] => {
return [first, second];
};
const result = pair(42, 'hello'); // type: [number, string]
Это особенно полезно при работе с утилитами вроде map, reduce или при создании обёрток.
Проблемы с JSX (React) и их решение
Если вы пишете на React с TypeScript, то заметите, что синтаксис <T>(arg) компилятор может интерпретировать как JSX-тег. Это классическая проблема, из-за которой вопрос на Stack Overflow набрал такую популярность.
Есть два основных решения:
1. Использовать extends (даже если не нужно)
Добавьте extends unknown или extends Record<string, never>:
const myFunction = <T extends unknown>(arg: T): T => {
return arg;
};
Это явно указывает TypeScript, что <T> — это дженерик, а не JSX.
2. Использовать синтаксис с запятой
В некоторых случаях помогает добавление запятой после дженерика:
const myFunction = <T,>(arg: T): T => {
return arg;
};
Запятая не влияет на логику, но решает проблему с парсингом JSX.
Также можно использовать ключевое слово function вместо стрелочной функции, если синтаксис становится слишком запутанным.
Типичные ошибки и как их избежать
Даже опытные разработчики иногда допускают эти ошибки. Давайте разберём их.
Ошибка 1: Забываем указать дженерик в типе
// ❌ Неправильно
const getLength = (arr: T[]): number => arr.length; // T не определён
// ✅ Правильно
const getLength = <T>(arr: T[]): number => arr.length;
Дженерик нужно объявить до того, как вы его используете.
Ошибка 2: Путаем синтаксис с обычной функцией
// ❌ Неправильно (синтаксис обычной функции)
const func = function<T>(arg: T): T { return arg; }; // Это корректно, но не стрелка
// ✅ Стрелочная функция
const func = <T>(arg: T): T => arg;
Разница только в положении дженерика: у стрелочных функций он стоит перед скобками, а не после ключевого слова function.
Ошибка 3: Неправильное использование в коллбэках
Иногда нужно передать стрелочную функцию с дженериком как коллбэк. Здесь важно правильно типизировать переменную:
// ❌ Неправильно
const processArray = <T>(arr: T[], callback: (item: T) => void): void => {
arr.forEach(callback);
};
// ✅ Правильно — TypeScript сам выведет тип T из массива
const processArray = <T>(arr: T[], callback: (item: T) => void): void => {
arr.forEach(callback);
};
В этом примере всё корректно, но если вы попытаетесь явно указать дженерик в месте вызова, будьте внимательны с синтаксисом.
Продвинутые примеры: когда дженерики в стрелочных функциях особенно полезны
Давайте рассмотрим несколько реальных сценариев.
Пример 1: Обёртка для асинхронных запросов
const fetchData = <T>(url: string): Promise<T> => {
return fetch(url).then(response => {