Kodokon kodokon.com

Продвинутые дженерики: ограничения и значения по умолчанию

Ты ограничишь свои параметры типа через extends и задашь значения по умолчанию, чтобы строить удобные обобщённые API.

10 мин · 3 вопросов

Открыть этот урок в Kodokon

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

TYPESCRIPT
function longest<T extends { length: number }>(
  a: T,
  b: T
): T {
  return a.length >= b.length ? a : b;
}

const word = longest("hello", "worldwide");
const list = longest([1, 2], [3, 4, 5]);
word остаётся типа string, а list - number[].

Связка K extends keyof T - самый выгодный повседневный паттерн: она связывает два параметра типа между собой и статически гарантирует, что ключ действительно принадлежит объекту. Возвращаемый тип T[K] (тип с индексным доступом) точно отслеживает запрошенное свойство. Именно так типизированы функции вроде pick или pluck из утилитарных библиотек.

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

const config = { port: 8080, host: "localhost" };
const port = getProp(config, "port");
const host = getProp(config, "host");
port выводится как number, host - как string; любой другой ключ будет отвергнут.

Значения по умолчанию (T = string) делают дженерик необязательным на месте вызова: аргумент типа указывают только тогда, когда хотят отойти от типичного случая. В сочетании с ограничением (T extends X = Y) они задают и границу, и неявное поведение. Значение по умолчанию должно удовлетворять ограничению, иначе не скомпилируется само объявление. Это стандартный паттерн для HTTP-клиентов, хранилищ и типизированных источников событий.

TYPESCRIPT
interface ApiResponse<T = unknown> {
  status: number;
  data: T;
}

async function fetchJson<T = unknown>(
  url: string
): Promise<ApiResponse<T>> {
  const res = await fetch(url);
  return { status: res.status, data: await res.json() };
}

interface Post { id: number; title: string }
const typed = await fetchJson<Post>("/api/posts/1");
const loose = await fetchJson("/api/health");
Без аргумента типа data становится unknown, а не any.

Проверка знаний

Убедись, что запомнил ключевые моменты этого урока.

  1. Что гарантирует ограничение K extends keyof T в обобщённой сигнатуре?
    • Что K - это любая строка
    • Что K - существующий ключ T, проверенный на этапе компиляции
    • Что T содержит хотя бы один необязательный ключ
    • Что K и T - один и тот же тип
  2. Какое преимущество даёт longest<T extends { length: number }> по сравнению с longest(a: { length: number }, b: ...) без дженериков?
    • Он работает быстрее
    • Он сохраняет точный тип вызывающей стороны в возвращаемом значении
    • Он принимает больше входных типов
  3. Почему в качестве значения по умолчанию лучше <T = unknown>, чем <T = any>?
    • unknown компилируется быстрее, чем any
    • any запрещён в строгом режиме
    • unknown заставляет вызывающего сузить тип перед использованием, а any отключает все проверки
    • unknown принимает меньше значений, чем any