Перейти к содержанию
Innovialabs

Дженерики в TypeScript: как писать переиспользуемый код

Практика3 мин

Коротко

Дженерик — это тип-параметр, который позволяет одной функции или типу работать с разными данными, не теряя точной типизации и не скатываясь в any.

Зачем нужны дженерики

Дженерик — это параметр для типа. Как функция принимает значения, так дженерик принимает типы. Это позволяет написать код один раз и использовать его с разными данными, сохраняя точные подсказки и проверки.

Без дженериков есть два плохих варианта: дублировать функцию под каждый тип или использовать any и потерять типизацию.

function first(items: any[]): any {
  return items[0];
}

const x = first([1, 2, 3]); // any — TypeScript больше ничего не проверяет

С дженериком тип сохраняется:

function first<T>(items: T[]): T | undefined {
  return items[0];
}

const n = first([1, 2, 3]);      // number | undefined
const s = first(['a', 'b']);     // string | undefined

T подставляется автоматически из аргумента — указывать его вручную обычно не нужно.

Обобщённые типы и интерфейсы

Самый частый практический пример — обёртка ответа API:

interface ApiResponse<T> {
  data: T;
  error: string | null;
}

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

async function getJson<T>(url: string): Promise<ApiResponse<T>> {
  const res = await fetch(url);
  if (!res.ok) return { data: null as T, error: `HTTP ${res.status}` };
  return { data: (await res.json()) as T, error: null };
}

const result = await getJson<User[]>('/api/users');
result.data[0].name; // string, работает автодополнение

Здесь T указан явно, потому что TypeScript не может узнать, что вернёт сервер. Помните: это обещание компилятору, а не проверка. Для данных извне в критичных местах добавляйте валидацию в рантайме (например, схемой).

Ограничения: extends

Иногда от типа нужно, чтобы у него было определённое поле. Для этого есть ограничение через extends:

function byId<T extends { id: string }>(items: T[], id: string): T | undefined {
  return items.find((item) => item.id === id);
}

Функция примет массив любых объектов, у которых есть id: string, и вернёт тот же тип, а не урезанный.

Ограничение с keyof позволяет безопасно брать поле объекта:

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

const user = { id: '1', name: 'Анна', age: 30 };
pick(user, 'name'); // string
pick(user, 'email'); // ошибка компиляции

Типы по умолчанию

Как у параметров функции, у дженериков бывают значения по умолчанию:

interface Paginated<T = unknown> {
  items: T[];
  page: number;
  total: number;
}

const a: Paginated = { items: [], page: 1, total: 0 };        // T = unknown
const b: Paginated<User> = { items: [], page: 1, total: 0 };  // T = User

Встроенные утилиты

TypeScript содержит готовые обобщённые типы. Они закрывают большинство повседневных задач:

УтилитаЧто делает
Partial<T>все поля необязательные — удобно для форм обновления
Required<T>все поля обязательные
Pick<T, K>оставляет только выбранные поля
Omit<T, K>убирает выбранные поля
Record<K, V>объект с ключами K и значениями V
ReturnType<F>тип результата функции
Awaited<T>тип, который получится после await
type UserUpdate = Partial<Omit<User, 'id'>>;
// { name?: string }

Полный список есть в официальной документации.

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

  • Дженерик ради дженерика. Если T встречается только один раз в сигнатуре, он, скорее всего, не нужен — достаточно обычного типа.
  • Слишком много параметров. <T, U, V, W> читать тяжело. Если без них нельзя, давайте осмысленные имена: TData, TError.
  • Каст вместо проверки. as T прячет ошибки данных. Для внешних данных нужна валидация.
  • Забытое ограничение. Без extends нельзя обращаться к полям T — компилятор не знает, что они есть.

Частые вопросы

any отключает проверку типов. Дженерик сохраняет конкретный тип и связывает вход с выходом: что передали, то и получили, со всеми подсказками и проверками.

Поможем с этим

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

Обсудить проект

Читайте также

Есть идея или задача? Расскажите нам

Ответим в течение рабочего дня и предложим решение. Консультация бесплатная.