Avatar ·

Синтаксис стрелочных функций с дженериками в TypeScript

Всем привет! Я начал изучать TypeScript и столкнулся с проблемой: не могу понять, как правильно писать стрелочные функции с дженериками (generic types). В обычных функциях всё понятно: ```typescript function identity(arg: T): T { return arg; } ``` Но когда я пытаюсь сделать то же самое со стрелочной функцией, TypeScript выдаёт ошибки. Пробовал разные варианты: ```typescript const identity = (arg: T): T => arg; // Ошибка: JSX element 'T' has no corresponding closing tag ``` ```typescript const identity = (arg: T): T => arg; // Работает, но выглядит странно ``` ```typescript const identity = (arg: T): T => arg; // Тоже работает, но не уверен, что это правильно ``` Подскажите, пожалуйста, как правильно объявлять стрелочные функции с дженериками? В каких случаях нужна запятая после ``? Есть ли разница между стрелочными и обычными функциями в этом плане? Заранее спасибо! <<>>

Проблема с синтаксисом JSX

Ошибка, которую вы видите, возникает из-за того, что TypeScript (и особенно TSX/JSX) интерпретирует <T> как открывающий тег JSX-элемента. Это классическая проблема синтаксической неоднозначности, когда угловые скобки используются и для дженериков, и для JSX.

Основные решения

Есть несколько проверенных способов объявить стрелочную функцию с дженериком:

  • Способ 1: Запятая после дженерика — самый популярный и рекомендованный в большинстве проектов:
const identity = <T,>(arg: T): T => {
    return arg;
};

// Использование:
const result = identity<string>("hello"); // type: string
const num = identity(42); // type: number (вывод типов работает)

Запятая <T,> сообщает парсеру, что это не JSX-тег, а параметр дженерика. Это самый лаконичный и широко используемый подход.

  • Способ 2: Ограничение через extends — явно указываем, что T — это тип, а не JSX-элемент:
const identity = <T extends unknown>(arg: T): T => {
    return arg;
};

// Более практичный пример с ограничением:
const getLength = <T extends { length: number }>(arg: T): number => {
    return arg.length;
};

getLength("hello"); // 5
getLength([1, 2, 3]); // 3
// getLength(42); // Ошибка: number не имеет свойства length
  • Способ 3: Типизация через переменную — объявляем тип отдельно:
type IdentityFn = <T>(arg: T) => T;

const identity: IdentityFn = (arg) => {
    return arg;
};

Реальные примеры использования

Вот более сложные сценарии, где дженерики в стрелочных функциях особенно полезны:

Пример 1: Функция-обёртка для React-компонента

// Создаём HOC (Higher-Order Component) с дженериком
const withLogger = <P extends object>(Component: React.ComponentType<P>) => {
    return (props: P) => {
        console.log("Props:", props);
        return <Component {...props} />;
    };
};

// Использование:
interface UserProps { name: string; age: number; }
const UserComponent = ({ name, age }: UserProps) => <div>{name}: {age}</div>;
const UserWithLogger = withLogger(UserComponent);

Пример 2: Типобезопасный обработчик событий

const createEventHandler = <T extends HTMLElement, E extends keyof HTMLElementEventMap>(
    element: T,
    eventType: E,
    handler: (event: HTMLElementEventMap[E]) => void
) => {
    element.addEventListener(eventType, handler as EventListener);
    return () => element.removeEventListener(eventType, handler as EventListener);
};

// Использование:
const cleanup = createEventHandler(document.body, "click", (event) => {
    console.log(event.clientX); // TypeScript знает, что event — MouseEvent
});

Важные замечания

  • Вывод типов работает: если TypeScript может вывести тип из аргументов, указывать его явно не обязательно: identity(42) вернёт number.
  • Множественные дженерики: запятая нужна только после первого, если есть риск путаницы с JSX: <T, U extends T>(a: T, b: U) => ....
  • В файлах .tsx всегда используйте запятую или extends — это стандарт для React-проектов.
  • Производительность: все способы компилируются в одинаковый JavaScript, разница только в синтаксисе.

Лично я рекомендую способ с запятой (<T,>) — он минималистичный, понятный и не создаёт лишних ограничений. Используйте его в качестве основного, а extends — когда нужно явно ограничить допустимые типы.

<<>>

Альтернативный подход: использование интерфейсов

Если вы предпочитаете более декларативный стиль или работаете с большим количеством однотипных функций, можно вынести сигнатуру дженерика в интерфейс:

interface IIdentity {
    <T>(arg: T): T;
}

const identity: IIdentity = (arg) => arg;

// Или с несколькими параметрами:
interface IMap {
    <TInput, TOutput>(array: TInput[], fn: (item: TInput) => TOutput): TOutput[];
}

const map: IMap = (array, fn) => array.map(fn);

Когда это полезно

  • Переиспользование типов: одна сигнатура для нескольких реализаций.
  • Документирование API: интерфейс служит контрактом.
  • Совместимость с классами: если функция будет передаваться как метод объекта.

Недостатки

  • Дополнительный код: нужно объявлять интерфейс отдельно.
  • Меньше гибкости: сложнее добавить новые ограничения на месте.

Этот подход хорошо работает в крупных проектах, где важна читаемость и поддержка типов. Для быстрых утилит лучше использовать синтаксис с запятой.