Avatar ·

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

Всем привет! Я новичок в TypeScript и никак не могу разобраться с синтаксисом стрелочных функций, когда нужно использовать дженерики (обобщённые типы). В обычных функциях я пишу так — всё работает: ```typescript function identity(arg: T): T { return arg; } ``` Но когда я пытаюсь сделать то же самое со стрелочной функцией, TypeScript выдаёт ошибки. Я пробовал несколько вариантов: ```typescript // Вариант 1 - ошибка const identity = (arg: T): T => arg; // Вариант 2 - тоже не работает const identity = (arg: T): T => arg; // Вариант 3 - ругается на JSX const identity = (arg: T): T => arg; ``` Конкретно в React компонентах с JSX у меня вообще каша в голове. Когда я пишу `` внутри JSX, TypeScript думает, что это JSX-элемент, а не дженерик. Как правильно объявлять стрелочные функции с обобщениями в TypeScript? И есть ли разница в синтаксисе для обычного TypeScript и для файлов с JSX (.tsx)? Заранее спасибо за помощь! <<>>

Основная проблема: конфликт с JSX

Вы почти всё сделали правильно. Главная причина, по которой ваш первый вариант не работает — это конфликт синтаксиса дженериков с JSX. В файлах .tsx угловые скобки <T> интерпретируются как JSX-тег, а не как указание обобщённого типа. Решение — добавить запятую после имени типа или использовать ограничение extends.

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

Вариант 1: Запятая после имени типа (рекомендуется)

const identity = <T,>(arg: T): T => {
  return arg;
};

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

Запятая <T,> сообщает TypeScript, что это обобщение, а не JSX-тег. Это самый чистый способ.

Вариант 2: Ограничение через extends

const identity = <T extends unknown>(arg: T): T => {
  return arg;
};

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

Вариант 3: Для нескольких дженериков

const pair = <T, U>(first: T, second: U): [T, U] => {
  return [first, second];
};

Реальный пример из React (файл .tsx)

import React from 'react';

// Компонент с дженериком
const ListComponent = <T extends { id: number }>({ items }: { items: T[] }) => {
  return (
    <ul>
      {items.map(item => (
        <li key={item.id}>{JSON.stringify(item)}</li>
      ))}
    </ul>
  );
};

// Использование
interface User {
  id: number;
  name: string;
}

const users: User[] = [
  { id: 1, name: "Alice" },
  { id: 2, name: "Bob" }
];

const App = () => {
  return <ListComponent<User> items={users} />;
};

Важные моменты

  • Вывод типов работает: если TypeScript может определить тип из аргументов, указывать его явно необязательно.
  • В обычных .ts файлах: можно писать <T> без запятой, но для единообразия лучше всегда добавлять запятую.
  • Стрелочные функции vs обычные: синтаксис дженериков идентичен, разница только в форме записи функции.
  • Ограничения (extends): используйте, когда нужно гарантировать наличие определённых свойств у типа.

Распространённые ошибки

// ❌ Ошибка: T воспринимается как JSX-компонент
const bad = <T>(arg: T) => arg;

// ✅ Правильно: с запятой
const good = <T,>(arg: T) => arg;

// ✅ Правильно: с extends
const good2 = <T extends string | number>(arg: T) => arg;

В итоге: для стрелочных функций с дженериками всегда используйте <T,> (с запятой) или <T extends ...>. Это универсальное решение, которое работает и в .ts, и в .tsx файлах.

<<>>

Альтернативный подход: объявление типа отдельно

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

// Определяем тип функции с дженериком
type IdentityFn = <T>(arg: T) => T;

// Реализуем функцию через этот тип
const identity: IdentityFn = (arg) => arg;

// Использование
const str = identity("hello"); // string
const num = identity(123);     // number

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

Для сложных компонентов React

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

interface TableProps<T> {
  data: T[];
  columns: (keyof T)[];
  renderCell: (item: T, column: keyof T) => React.ReactNode;
}

const Table = <T extends Record<string, any>>(props: TableProps<T>) => {
  // реализация
};

Такой подход делает код чище и отделяет логику типов от реализации функции.