Generic функции

Generic функции — функции с типовыми параметрами <T>, которые работают с разными типами данных, сохраняя связь между входными и выходными типами, которую компилятор выводит автоматически при вызове.

Зачем нужно

Без generics универсальные функции пишут с any, теряя типобезопасность. Generic функции позволяют написать код один раз и использовать с разными типами, при этом TypeScript отслеживает конкретный тип в каждом вызове. Это основа переиспользуемых утилит.

Где используется

  • Утилиты коллекций: first, last, groupBy, unique
  • Обёртки над fetch/API с типизированным ответом
  • HOF (функции высшего порядка): map, filter, reduce
  • Функции трансформации и маппинга данных
  • Promise-утилиты: retry, timeout, memoize

Основной контент

Базовый синтаксис

// Обычная функция
function identity<T>(arg: T): T {
  return arg;
}

// Стрелочная функция (в .tsx нужно <T,> или <T extends unknown>)
const identity = <T>(arg: T): T => arg;

// Явный вызов с типом
const s = identity<string>("hello"); // string
// Вывод типа (type inference) — TypeScript определяет T автоматически
const n = identity(42); // number

Несколько типовых параметров

function pair<A, B>(a: A, b: B): [A, B] {
  return [a, b];
}

const p = pair("name", 42); // [string, number]
const q = pair(true, { id: 1 }); // [boolean, { id: number }]

Generic функции с ограничениями

function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
  return obj[key];
}

const user = { name: "Alice", age: 30 };
const name = getProperty(user, "name"); // string
const age  = getProperty(user, "age");  // number
// getProperty(user, "missing"); // Error — "missing" не в keyof User

Практические утилиты

// first — первый элемент массива
function first<T>(arr: T): T | undefined {
  return arr[0];
}

// unique — дедупликация
function unique<T>(arr: T): T {
  return [...new Set(arr)];
}

// groupBy — группировка по ключу
function groupBy<T, K extends string | number>(
  arr: T,
  keyFn: (item: T) => K
): Record<K, T> {
  return arr.reduce((acc, item) => {
    const key = keyFn(item);
    (acc[key] ??= ).push(item);
    return acc;
  }, {} as Record<K, T>);
}

const users = [
  { name: "Alice", role: "admin" },
  { name: "Bob",   role: "user" },
  { name: "Carol", role: "admin" },
];
const byRole = groupBy(users, (u) => u.role);
// { admin: [...], user: [...] }

Generic fetch-обёртка

async function fetchJson<T>(url: string): Promise<T> {
  const res = await fetch(url);
  if (!res.ok) throw new Error(`HTTP ${res.status}`);
  return res.json() as Promise<T>;
}

interface User { id: number; name: string; }

const user = await fetchJson<User>("/api/users/1");
// user: User — типобезопасно
console.log(user.name);

Вывод типов в generic функциях

// TypeScript выводит T из аргументов
function wrap<T>(value: T): { value: T } {
  return { value };
}

const a = wrap("hello"); // { value: string }
const b = wrap([1, 2, 3]); // { value: number }

// Явное указание нужно когда inference не работает
const c = wrap<string | number>("hello"); // { value: string | number }

Частые ошибки

  • Писать <T extends any> — это то же что не писать ограничение вообще; лучше <T> или конкретный constraint.
  • Не использовать K extends keyof T для доступа к полям объекта — без этого TypeScript не сможет вывести тип возвращаемого значения.
  • Дублировать код вместо generic — если функции getNumber и getString отличаются только типом, это кандидат на generic.
  • Усложнять generics без необходимости — если функция принимает и возвращает одно и то же, <T> уместен; если возврат всегда string — generic избыточен.

Связанные темы

Ресурсы