Переиспользуемый компонент на TypeScript обязан переживать любые формы пропсов, и обычных типов для этого мало: таблица берёт строки любой формы, поле формы берёт объект с любими полями, запрос берёт любой ответ. Generics решают ровно эту задачу: тип становится параметром, компонент или функция описывает форму через переменную типа, и каждая точка использования получает точную типизацию без дублирования описаний. Написано один раз, типизировано везде.

Фронтенд с generics выглядит иначе: подсказки в редакторе знают поля каждой сущности, опечатка в имени колонки подсвечивается до запуска, рефакторинг сущности проезжает по всем таблицам за минуту. Ниже разобраны базовая механика, типизированные пропсы, ограничения и keyof, generic слой запросов, типы-утилиты, честное сравнение с дискриминированными объединениями и границы разумного, за которыми начинается головоломка вместо пользы.

Что generics дают фронтенду помимо сложных угловых скобок

Переменная типа это плейсхолдер, который заполняется в момент использования. Функция сортировки, описанная над массивом элементов переменного типа, работает и с пользователями, и с товарами, и редактор знает, на что опирается сравнение в каждом случае. Компонент таблицы с переменной типа строки принимает любые списки, а имена колонок сверяются с полями конкретной сущности, и опечатка в имени поля ловится компилятором, а не пустой колонкой на экране.

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

Второй выигрыш это граница слоёв: generic слой запросов описывает один раз, что успешный ответ имеет форму переменного типа, и каждый вызов сужает тип до конкретной сущности. Контракт фиксируется в одном месте, а не в двадцати обработчиках ответов, и изменение контракта сразу подсвечивает все затронутые экраны.

Третий выигрыш это документация: сигнатура generic функции читается как описание поведения, и новый разработчик понимает компонент по типам пропсов без раскопок по реализации. Типы это первый текст, который читают в чужом коде.

Типизированные пропсы компонента от таблицы до полей формы

Компонентная типизация начинается с пропсов, описанных через переменную типа. Таблица объявляет, что принимает массив строк любого вида и список колонок, где ключ обязан быть ключом строки, и это ограничение делает невозможной колонку по несуществующему полю.

type TableProps<T> = {
  rows: T[];
  columns: Array<{ key: keyof T; title: string }>;
};

export function Table<T>({ rows, columns }: TableProps<T>) {
  return (
    <table>
      <tr>
        {columns.map((c) => (
          <th key={String(c.key)}>{c.title}</th>
        ))}
      </tr>
      {rows.map((row, i) => (
        <tr key={i}>
          {columns.map((c) => (
            <td key={String(c.key)}>{String(row[c.key])}</td>
          ))}
        </tr>
      ))}
    </table>
  );
}

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

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

Выведение типа работает за разработчика: точка использования не обязана указывать переменную типа явно, компилятор выводит её из аргументов вызова, и сигнатура остаётся короткой. Явная аннотация нужна на границах, где выводу нечем руководствоваться, и там она честно несёт знание о сущности.

Ограничения extends keyof и условные типы как язык библиотек

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

function pluck<T, K extends keyof T>(items: T[], key: K): T[K][] {
  return items.map((item) => item[key]);
}

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

Условные типы доводят выразительность до библиотечного уровня: тип проверяет условие и выбирает одну из двух ветвей, а слово вывода вытаскивает внутренний тип из обёртки. На условных типах пишутся утилиты, которые достают тип промиса, тип элемента массива, тип ответа запроса, и пользовательский код получает готовый результат без ручных распаковок.

Маппированные типы перестраивают формы: описанное правило применяется к каждому ключу исходного объекта, и на свет появляется производный тип, например все поля необязательные или все поля только для чтения. Связка из ограничения, условия и перестроения описывает transform компонентов, фильтров и сериализаторов в нескольких строках, и эти строки читаются годами без сюрпризов.

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

Generic API слой fetchJson и контракт ответов без дублирования

Слой запросов это место, где generics окупаются быстрее всего. Функция обёртки описывает форму успешного ответа переменной типа, проверяет статус, превращает отказы в типизированную ошибку, и каждый вызов сообщает конкретную сущность.

class HttpError extends Error {
  constructor(public status: number) {
    super("http " + status);
  }
}

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

const user = await fetchJson<User>("/api/user/7");

Слой ошиб типизируется рядом с ответами: объединение успешного и неуспешного исходов описывает обе ветви, и экран обязан обработать отказ, компилятор не даст забыть. Пустой ответ, статус отказа и сетевая авария становятся разными ветвями одного описания, и обработка перестаёт быть свалкой догадок.

Хуки сведений собираются поверх слоя: хранилище запроса с переменной типа хранит состояние загрузки, готового значения и ошибки, и компонент получает готовую связку без копирования логики по экранам. Библиотеки запросов работают именно так, и собственный облегчённый вариант пишется за вечер.

Контракты сущностей живут рядом с generic слоем и меняются в одном месте: поле переименовано в описании, и каждое обращение через слой подсвечивается за минуту. Без generics переименование обнаруживается на рантайме по участившимся пустым экранам, и разница между этими режимами и есть разница между доверием и надеждой.

Типы утилиты Partial Pick Omit Record и их время применения

Стандартная библиотека типов экономит место, если знать её наизусть. Частичные пропсы для форм редактирования это утилита, делающая все поля необязательными, и одна строчка заменяет ручную копию описания с вопросиками. Выбор полей для колонок таблицы и списков это утилиты извлечения и удаления полей, и производные описания следуют за исходным автоматически.

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

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

Собственные утилиты проекта описываются рядом со стандартными и называются по той же схеме: тип результата выборки, тип ответа с разбиением на страницы, тип колонки таблицы. Когда проект называет свои сущности одинаково со стандартной библиотекой, чтение кода перестаёт переключаться между двумя словарями.

Дискриминированные объединения против generics когда что выбирать

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

Состояния запроса это классика объединений: загрузка, готово, отказ, у каждого варианта своя пара полей, и switch по метке сужает тип автоматически. Пытаться выразить состояния generics значит потерять сужение и получить поля, которых нет в текущем варианте. Обратная ошибка выражать сущности объединениями раздувает описания до перечисления всех комбинаций и убивает переиспользование.

Совместная работа механизмов это норма зрелого кода: хранилище запроса generic по сущности и объединённое по состоянию, и одно описание обслуживает и формы, и жизненный цикл. Компонент выбора получает generic список, а его внутренние состояния остаются честным объединением с меткой.

Практическое правило звучит коротко: если перечисление вариантов конечно и известно заранее, это объединение, если набор форм открыт и расширяется потребителями, это generics. Смешанные случаи решаются раскладкой на слои, и слои почти всегда отвечают разным вопросам.

Пределы generics переиспользуемость без вырождения в головоломку

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

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

Третий предел это границы слоёв: generics живёт внутри слоя, а на границах типы называются явно. Экспортируемая функция с четырьмя переменными типа это паспорт головной боли для всех потребителей, и публичные описания продуктовых библиотек делаются максимально плоскими.

Четвёртый предел это обучение: команда, для которой generics нов, осваивает его по одной механике за спринт, и порядок начинается с пропсов и утилит, продолжается ограничениями и заканчивается условными типами. Внедрение без этапности рождает копипасту хитрых сигнатур без понимания, а это хуже отсутствия generics вовсе.

Порядок внедрения generics в проект свёрнут в короткую лестницу:

  1. описать пропсы переиспользуемых компонентов переменными типа с выводом;
  2. добавить ограничения keyof к местам доступа по именам полей;
  3. собрать generic слой запросов с типизированной ошибкой;
  4. заменить производные описания типами-утилитами от основных сущностей;
  5. выразить состояния объединениями с меткой рядом с generic сущностями;
  6. вывести собственные утилиты проекта и назвать их в стиле стандартных;
  7. провести ревью сложности и упростить каждую сигнатуру, не читаемую одним предложением.

Generics это не сложность ради сложности, а способ описать переиспользуемость так, чтобы компилятор её проверял. Компонент, который типизирован переменной типа, работает со всеми будущими сущностями проекта без единой новой строки, и именно это свойство делает generics главным инструментом фронтенда, который растёт годами без переписывания основ.