Дженерики (Generics)

Дженерики (Generics)

Технический фундамент параметров типа

Generics дают способ связать несколько мест контракта одним типовым параметром:

  • вход и выход функции могут быть строго связаны через T;
  • ограничения (extends) защищают от слишком широких сценариев;
  • вывод generic-типа часто происходит автоматически из аргументов;
  • цель дженерика не "усложнить тип", а сохранить точность в переиспользуемом API.

Если generic не связывает реальные части контракта, он обычно избыточен.

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

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

Ключевой момент: дженерик это "параметр типа", как переменная, но для типов.

Проверь себя: почему any в утилитарной функции может сломать типовую безопасность всего потока данных?

Базовый пример generic-функции

function identity<T>(value: T): T {
  return value;
}

const a = identity<number>(10);
const b = identity('hello'); // тип выведется автоматически

Смотри, что важно: функция возвращает точно тот же тип, который получила.

Generics и массивы

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

const firstUser = firstItem([{ id: 1 }, { id: 2 }]);
const firstName = firstItem(['Ann', 'Max']);

Одна функция работает для разных типов массива без потери точности.

Ограничения generic (extends)

Иногда generic слишком общий, и нужны ограничения.

function getId<T extends { id: string | number }>(entity: T) {
  return entity.id;
}

Новый термин: constraint - ограничение на допустимые типы параметра T.

Проверь себя: зачем ограничивать T, если можно оставить его любым?

keyof и безопасный доступ к свойствам

Частый прод-паттерн: написать функцию, которая берет объект и имя поля, и при этом возвращает правильный тип значения этого поля.

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

const user = { id: 1, name: 'Anna' };

const id = getProp(user, 'id'); // number
const name = getProp(user, 'name'); // string
// getProp(user, 'age'); // Ошибка: "age" нет в keyof typeof user

Смотри, что важно: keyof T это "множество" (union) допустимых ключей, а T[K] это тип значения по ключу.

Generic-интерфейсы и типы

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

const userResponse: ApiResponse<{ id: number; name: string }> = {
  data: { id: 1, name: 'Anna' },
  error: null,
};

Это частый прод-сценарий: единая форма ответа с разным типом data.

Generics в классах

class StorageBox<T> {
  private value: T;

  constructor(value: T) {
    this.value = value;
  }

  getValue(): T {
    return this.value;
  }
}

const numBox = new StorageBox<number>(42);

Класс становится переиспользуемым и остается типобезопасным.

Реальные микро-сценарии

  1. Универсальная функция fetchJson<T>(), возвращающая типизированные данные API.
  2. Generic-таблица UI, где строки имеют разные структуры в разных экранах.
  3. Кеш-хранилище, где тип значения задается при создании.

Частые ошибки новичков

  • Использовать generic, когда достаточно конкретного типа.
  • Забывать ограничения extends и получать слишком широкий контракт.
  • Перегружать код сложными generic-цепочками без пользы.
  • Подменять generics any, теряя все преимущества.

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

Что будет, если изменить входные данные

Если identity получает объект { id: 1 }, TypeScript сохранит его точную форму на выходе. Если функция была бы на any, ты легко потерял бы свойства или передал бы несовместимый тип дальше без предупреждения.

Проверь себя: в каком случае generic помогает рефакторить без страха сломать типы в цепочке вызовов?

Краткий итог

  • Generics позволяют писать универсальный код без потери типовой точности.
  • Параметр типа T связывает входы и выходы функции/класса.
  • Ограничения extends делают generic-контракты безопаснее.
  • Generic-интерфейсы особенно полезны для API, утилит и инфраструктурного кода.
  • Главная цель дженериков - переиспользование без отказа от строгой типизации.