Ограничение дженерика задаёт, каким может быть параметр типа. <T extends { length: number }> принимает только типы с числовым length (строки, массивы, ваши собственные объекты), и благодаря этому обещанию функции разрешено читать .length.
Возвращаемый тип по-прежнему T, а не { length: number }: если передать два массива чисел, вернётся number[], а объекты сохранят свой tag. Ограничение описывает минимум; тип вызывающего кода сохраняется.
Зачем нужны ограничения
Внутри обобщённой функции неограниченный T может быть чем угодно: строкой, числом, null, функцией. Поэтому TypeScript разрешает только то, что работает на любом типе, а это почти ничего.
Компилятор выводит:
index.ts(3,12): error TS2339: Property 'length' does not exist on type 'T'.
index.ts(3,24): error TS2339: Property 'length' does not exist on type 'T'.
Исправление: сказать, что вам нужно, <T extends { length: number }>. Ограничения превращают «любой тип» в «любой тип, у которого есть это», а обычно обобщённая функция именно это и имеет в виду.
Ограничение интерфейсом
Ограничением может быть любой тип, включая уже существующий интерфейс или псевдоним типа. Функция тогда работает со всем, у чего есть как минимум эти члены, и по-прежнему возвращает полный тип вызывающего кода.
Сравните с обычным типом параметра: function byId(items: HasId[], id: number): HasId | undefined принимает те же массивы, но результат будет только HasId, и found.name дал бы ошибку. Обобщённая версия пропускает полный тип насквозь.
K extends keyof T: безопасный доступ к свойствам
Самое частое ограничение связывает два параметра типа. K extends keyof T говорит, что K должен быть одним из имён свойств T, а T[K] это тип этого свойства.
Ключ с опечаткой или отсутствующий ключ даёт ошибку компиляции, а возвращаемый тип следует за ключом: "year" даёт number, "title" даёт string. Сам keyof, включая его поведение с индексными сигнатурами, описан на странице keyof.
Ограничение примитивами
Ограничением может быть примитив или их объединение. Полезный побочный эффект: когда T extends string (или number), TypeScript выводит литеральный тип аргумента, а не расширяет его.
Без ограничения createEvent("user.saved") возвращает { type: string }: здесь TypeScript расширяет выведенный для T литерал до string. С T extends string литерал сохраняется, и именно так типизированные помощники для событий и маршрутизации получают точные имена из обычных строковых аргументов.
Частые ошибки
Возврат того, что соответствует только ограничению. Функция, которая возвращает T, должна вернуть T, а не любое значение, подходящее под ограничение:
interface HasId {
id: number;
}
function reset<T extends HasId>(item: T): T {
return { id: item.id };
}
// error TS2322: Type '{ id: number; }' is not assignable to type 'T'.
// '{ id: number; }' is assignable to the constraint of type 'T', but 'T' could be
// instantiated with a different subtype of constraint 'HasId'.
Если T это { id: number; name: string }, у нового объекта нет name, поэтому он не является T. Верните развёрнутую копию входа, return { ...item, id: 0 };, которая сохраняет все свойства, или объявите возвращаемым типом HasId, если больше вы ничего не создаёте.
Аргумент типа, нарушающий ограничение. Явные аргументы типа тоже проверяются: при function scale<T extends number>(x: T) вызов scale<string>("2") падает с Type 'string' does not satisfy the constraint 'number'. (TS2344).
Ограничение там, где дженерик не нужен. Если T встречается только в параметре, function print<T extends HasId>(item: T): void это просто function print(item: HasId): void с лишним синтаксисом. Ограничения важны, когда T встречается ещё и в возвращаемом типе или в другом параметре.
Ограничения со значениями по умолчанию
У параметра типа может быть и ограничение, и значение по умолчанию. Значение по умолчанию должно удовлетворять ограничению.
interface Store<TState extends object = Record<string, unknown>> {
get(): TState;
set(next: Partial<TState>): void;
}
type AnyStore = Store; // Store<Record<string, unknown>>
type CounterStore = Store<{ count: number }>; // fine
type BadStore = Store<number>;
// error TS2344: Type 'number' does not satisfy the constraint 'object'.
То же ключевое слово extends встречается и в условных типах, T extends string ? A : B, где это проверка, а не ограничение; эта форма описана на странице условные типы.
Часто задаваемые вопросы
Что означает T extends в дженерике TypeScript?
<T extends Constraint> означает, что T может быть любым типом, который можно присвоить Constraint. Вызывающий код может передавать только такие типы, а внутри функции можно пользоваться всем, что гарантирует ограничение. <T extends { length: number }> принимает строки, массивы и любой объект с числовым length и позволяет читать value.length.
Что означает K extends keyof T?
K должен быть одним из имён свойств T. Вместе с индексированным типом доступа T[K] это точно типизирует доступ к свойству: function get<T, K extends keyof T>(obj: T, key: K): T[K] принимает только настоящие ключи obj, а возвращаемый тип это тип соответствующего свойства.
Почему я получаю «Property does not exist on type T»?
Неограниченный T может быть любым типом, включая number или null, поэтому TypeScript не разрешает с ним ничего специфичного для типа (TS2339). Добавьте ограничение, которое обещает свойство: <T extends { name: string }>, или ограничьте уже существующим интерфейсом, <T extends User>.
Что означает «T could be instantiated with a different subtype of constraint»?
Ошибка TS2322: функция обещает вернуть T, а возвращает значение, которое соответствует только ограничению. Если T это { id: number; name: string }, то новый { id: 1 } не является T. Верните полученное значение (или его развёрнутую копию с соответствующим типом) либо поменяйте возвращаемый тип на ограничение.
Чем extends в дженерике отличается от extends в классе или интерфейсе?
Ключевое слово то же, идея родственная. В class Dog extends Animal и interface B extends A оно объявляет наследование. В <T extends A> оно объявляет ограничение: T должен быть присваиваемым A. В условном типе, T extends A ? X : Y, это проверка, которая выбирает один из двух типов.