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; если сигнатура не выражает реализацию, сначала пересмотрите отношения между параметрами.
Что важно запомнить
- Generic нужен для сохранения отношения между типами, а не для отключения проверок.
- Вывод типа обычно делает вызов короче; явный type argument требуется не всегда.
extendsзадаёт минимальные возможностиT,keyof T— допустимые ключи, аT[K]— тип выбранного свойства.- Статические параметры типов исчезают в JavaScript и не заменяют runtime-валидацию внешних данных.
Связанные исследования
- Структурная типизация и generics в TypeScript: границы обещаний компилятора — Связываем структурную совместимость и generics TypeScript с теорией полиморфизма: keyof, T[K], variance, brands, runtime-границы и ограничения soundness.