Обобщения

Что это такое

Обобщение задаёт параметр типа. Это заполнитель для типа, который подставляет вызывающий код, и в этом его отличие от объединения: объединение перечисляет возможности заранее, а обобщение оставляет связь открытой — тип, поданный на вход, остаётся связан с типом, который выходит наружу. Array<T> — повседневный пример, а function first(items: T[]): T | undefined — тот, что вы пишете сами.

Почему это важно

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

Ключевые идеи

Обобщение описывает связь, а не категорию

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

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

const name = first(["a", "b"]);   // string | undefined, выведено компилятором

Ограничивайте тем, что нужно, и ничем больше

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

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

pluck(users, "id");       // string[] по выводу типов
pluck(users, "missing");  // ошибка компиляции — такого ключа у User нет

Здесь обобщения и окупаются: опечатка в имени свойства становится ошибкой компиляции, а не списком undefined.

Позвольте работать выводу; явные параметры — только на границах

TypeScript выводит аргументы типа из переданных аргументов, и писать их явно приходится лишь изредка — например, когда тип параметра обратного вызова иначе остался бы неявным; большинство же явных аргументов типа в прикладном коде говорят о том, что сигнатура слишком умна.

Утилитарные обобщённые типы составляются

Mapped-типы стандартной библиотеки строят новые формы из существующих — Partial<T> делает всё необязательным, Pick<T, K> выбирает, Omit<T, K> удаляет, Record<K, V> отображает ключи в значения — и читаются как предложение, которое описывают, избавляя от ручного дублирования формы.

type UserPreview = Pick<User, "id" | "name">;
type EditableUser = Omit<User, "createdAt">;

Чрезмерно обобщённый код — настоящий режим отказа

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

Практика

Напишите типизированный помощник поиска

Напишите функцию, которая принимает массив записей и ключ и возвращает различные значения по этому ключу, сохраняя тип значения.

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

Замените помощник с any на обобщённый

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

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

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

  • Параметр типа используется один раз — вывести ничего нельзя, и вызывающему коду приходится аннотировать без всякой выгоды; исправить: возьмите конкретный тип или найдите второе место, где связь действительно нужна.
  • Ограничение конкретным типом — T extends string там, где тело не использует строковых операций, сужает функцию без причины; исправить: ограничивайте только тем, что использует тело.
  • Однобуквенные параметры типа в длинных сигнатурах — T, U и V становятся неразличимы; исправить: используйте осмысленные имена вроде TItem или TResult, как только параметров больше одного.
  • Условные типы там, где хватит объединения — сигнатура становится нечитаемой, а сообщения об ошибках — непригодными; исправить: сначала решите задачу объединением или перегрузкой.
  • Дублирование формы вместо вывода из неё — копия расходится с оригиналом; исправить: выводите форму через Pick, Omit или Partial.

Что читать дальше