Обобщения — Примеры

Пример 1 — Помощник поиска, теряющий тип значения

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

function pluck(items: any[], key: string): any[] {
  return items.map((item) => item[key]);
}

const ids = pluck(users, "id") as string[];    // приведение в каждом месте вызова
const bad = pluck(users, "idd");               // тоже компилируется; возвращает [undefined]

Решение

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

const ids = pluck(users, "id");    // string[] по выводу типов
pluck(users, "idd");
             ~~~~~
error TS2345: Argument of type '"idd"' is not assignable to parameter of type 'keyof User'.

Обобщение описывает связь, и здесь параметр типа встречается дважды: один раз как тип элемента на входе, один раз как тип значения на выходе, а ключ ограничен существующими свойствами. Именно поэтому компилятор выводит string[] прямо в месте вызова, и приведение не нужно; ограничение тоже важно — keyof T требует ровно того, что использует тело, и ничем больше, а расширение сигнатуры до any не сделало функцию переиспользуемой: оно переложило на каждый вызывающий код ответственность за тип, который функция и так знала.

Пример 2 — Форма предпросмотра, скопированная вручную

Тип предпросмотра выписали отдельным объявлением, и он уже отстал от исходного типа; скажите, что будет происходить дальше, и выведите форму вместо её повторения.

type User = { id: string; name: string; email: string; createdAt: string; deletedAt: string | null };

type UserPreview = { id: string; name: string };   // копия, поддерживаемая вручную

Решение

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

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