Обобщения
Что это такое
Обобщение задаёт параметр типа. Это заполнитель для типа, который подставляет вызывающий код, и в этом его отличие от объединения: объединение перечисляет возможности заранее, а обобщение оставляет связь открытой — тип, поданный на вход, остаётся связан с типом, который выходит наружу. 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.
Что читать дальше
- Сужение типов и объединения (02-frontend/19-typescript/02-narrowing-and-unions) —
keyofи ограниченные параметры типа сужают так же, как дискриминант. - Конфигурация и миграция (02-frontend/19-typescript/04-configuration-and-migration) — параметры компилятора, которые решают, насколько строго проверяются эти связи.
- Чистый код (01-cs-fundamentals/08-clean-code) — то же суждение о том, когда абстракция окупает свою цену.