ЯдроКодаподготовка к экзаменам
Учебная платформа

Загружаем материалы

Подготавливаем материалы и навигацию по разделу.

Generics в TypeScript: параметры типов, extends и keyof

Автор: · Обновлено

Generics в TypeScript описывают связь между типами входных данных и результата, не теряя конкретную информацию о значении. Параметр типа T — не замена слову any: компилятор выводит его при каждом вызове и проверяет, что связанные позиции согласованы.

Зачем функции параметр типа

Функция first должна вернуть элемент именно того типа, который лежит в массиве. С unknown результат пришлось бы дополнительно сужать, а any отключил бы полезную проверку. Generic сохраняет отношение «тип результата равен типу элемента».

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

const firstPort = first([3000, 8080]);
const firstRole = first(["admin", "student"] as const);

console.log(firstPort); // number | undefined
console.log(firstRole); // "admin" | "student" | undefined

Код можно сохранить в generics.ts, проверить командой npx tsc --strict generics.ts и запустить доступным в проекте TypeScript runner. В большинстве вызовов T указывать не нужно: компилятор выводит его из аргумента. Явная запись вроде first<number>([1, 2]) полезна, когда выводу не хватает контекста, но не должна маскировать неверную модель типов.

Два параметра и точный тип результата

Параметры типов особенно полезны, когда связывают несколько значений. Для безопасного чтения свойства ключ K ограничивается множеством keyof T, а результат получает индексированный тип T[K].

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

const course = {
  title: "TypeScript без догадок",
  lessons: 12,
  published: true,
};

const title = getProperty(course, "title"); // string
const lessonCount = getProperty(course, "lessons"); // number

console.log(title, lessonCount);
// getProperty(course, "author"); // ошибка компиляции: такого ключа нет

Здесь extends object запрещает примитивы, а K extends keyof T разрешает только существующие ключи. Тип T[K] не является общим union всех полей: для литерала "lessons" компилятор получает именно number.

Ограничения extends описывают доступные операции

Внутри функции над неизвестным T нельзя вызвать .length: не всякое значение имеет это свойство. Ограничение фиксирует минимальный контракт, который нужен реализации.

type WithLength = { length: number };

function longest<T extends WithLength>(left: T, right: T): T {
  return left.length >= right.length ? left : right;
}

console.log(longest("cat", "typescript"));
console.log(longest([1, 2], [1, 2, 3]));

Возвращаемый тип остаётся T, поэтому нельзя вернуть произвольный объект { length: 10 }: он удовлетворяет ограничению WithLength, но может не содержать другие поля конкретного T. Ограничение говорит, чем функция вправе пользоваться, а не стирает исходный подтип.

Generic-типы для ответа и состояния

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

type Result<T, E = Error> =
  | { ok: true; value: T }
  | { ok: false; error: E };

function unwrapOr<T>(result: Result<T>, fallback: T): T {
  return result.ok ? result.value : fallback;
}

const loaded: Result<string[]> = { ok: true, value: ["intro", "types"] };
console.log(unwrapOr(loaded, []));

E = Error задаёт тип по умолчанию: его можно заменить, но при обычном использовании достаточно указать только T. Discriminated union по полю ok отвечает за ветвление, а generic передаёт конкретный тип успешного значения.

Где generics вредят выводу типов

Параметр типа должен связывать хотя бы две значимые позиции или сохранять информацию для вызывающего кода. Функция function log<T>(value: T): void, которая только печатает значение, обычно не выигрывает от T: достаточно unknown. Не стоит добавлять параметр только ради «универсальности».

Избыточные ограничения тоже ухудшают вывод. Сигнатура first<T extends any[]>(items: T) заставляет реализацию работать через широкое ограничение и способна дать менее точный элемент, чем простой first<T>(items: T[]). Предпочитайте минимум параметров и располагайте их ближе к реальным данным.

Типы TypeScript стираются при компиляции. Result<User> не проверяет JSON во время выполнения, а T extends object не валидирует ответ сервера. Данные с внешней границы всё равно требуют runtime-проверки до того, как им будет доверен статический тип.

Ошибки и диагностика

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

При неожиданно широком результате наведите курсор на каждый промежуточный тип, временно запишите ожидаемую аннотацию и уменьшите ограничение. Явный type argument используйте после проверки сигнатуры: он способен уточнить намерение, но также принудительно расширить литералы.

Практика: типобезопасный индекс

Реализуйте indexBy<T extends object, K extends keyof T>(items: readonly T[], key: K), которая строит Map<T[K], T>. Проверьте её на массиве пользователей с ключами id: number и email: string: для "id" ключ карты должен стать number, для "email"string. Добавьте два пользователя с одинаковым ключом и явно опишите выбранное правило: последний перезаписывает первого или функция возвращает ошибку.

Усложнение: ограничьте допустимый ключ только свойствами, пригодными для ключа Map в вашем домене. Не применяйте as и any; если сигнатура не выражает реализацию, сначала пересмотрите отношения между параметрами.

Что важно запомнить

Источники