Ограничения extends
Иногда обобщённой функции мало знать, что T — какой-то тип: ей нужно что-то от самого значения. Напишем функцию longest, которая возвращает более длинное из двух значений — строк или массивов:
function longest<T>(a: T, b: T): T {
return a.length >= b.length ? a : b; // ✗
}error TS2339: Property 'length' does not exist on type 'T'.TypeScript прав. Вместо T можно подставить любой тип: number, boolean, даже null. У числа нет length, и функция сломалась бы на первом же таком вызове. Пока о T ничего не известно, TypeScript не даёт читать у значения свойства.
Вернёмся в порт. Кран поднимает любой контейнер, у которого на крыше есть проушины для крюков. Крану всё равно, что внутри: мебель или бананы. Но контейнер без проушин он не возьмёт. Функции longest нужно то же самое: груз любой, лишь бы у него было свойство length.
Ограничение через extends
Ограничение параметра типа записывают словом extends после его имени. Оно говорит, какие типы можно подставить вместо T:
function longest<T extends { length: number }>(a: T, b: T): T {
return a.length >= b.length ? a : b;
}
console.log(longest("cat", "horse")); // horse
console.log(longest([1, 2], [3]).join(" ")); // 1 2Разберём запись T extends { length: number } по частям:
T— параметр типа, как раньше;extends— «подходит под»: подставить можно только такой тип, который подходит под тип справа;{ length: number }— требование: у значения должно быть свойствоlengthс числом.
Что это даёт:
- Внутри функции TypeScript знает, что у
aиbестьlength, и ошибки больше нет. longest("cat", "horse")проходит: у строк естьlength. Программа печатаетhorse.longest([1, 2], [3])тоже проходит: у массивов естьlength. Программа печатает1 2.
Слово extends вы уже видели у классов. Здесь оно не про наследование: строка ничего не наследует от { length: number }, у неё есть нужное свойство, и этого достаточно. TypeScript сравнивает типы по форме, как с интерфейсами.
Аргумент без length
Теперь передадим числа:
function longest<T extends { length: number }>(a: T, b: T): T {
return a.length >= b.length ? a : b;
}
console.log(longest(10, 200)); // ✗error TS2345: Argument of type 'number' is not assignable to parameter of type '{ length: number; }'.tsc сравнивает аргумент с ограничением, и number под { length: number } не подходит. Заметьте, где стоит ошибка: в строке вызова, а не в объявлении функции. Функция написана верно — неверен вызов, который нарушил её требование. Это контейнер без проушин: кран остановили ещё до подъёма.
Здесь аналогия заканчивается. Крановщик смотрит на сам контейнер, а tsc — только на тип значения. Значение с типом unknown он не пропустит, даже если на деле там строка.
Зачем T, если есть тип параметра
Можно ли обойтись без T и написать требование прямо в параметрах: function longest(a: { length: number }, b: { length: number })? Функция примет те же аргументы, но у результата будет тип { length: number }. TypeScript забудет, что пришли строки, и longest("cat", "horse").toUpperCase() не скомпилируется:
error TS2339: Property 'toUpperCase' does not exist on type '{ length: number; }'.С T extends { length: number } TypeScript помнит, что пришли строки: у результата строковый тип, и toUpperCase() компилируется. Ограничение говорит, что значению нужно уметь, а T помнит, чем оно было.
На собеседовании часто спрашивают, что делает extends в записи <T extends …>. Ответ: он ограничивает, какие типы можно подставить вместо T, и разрешает пользоваться внутри функции тем, что ограничение обещает.
Частая ошибка
Внутри функции взяли у значения то, чего ограничение не обещает:
function shout<T extends { length: number }>(x: T): string {
return x.toUpperCase(); // ✗
}error TS2339: Property 'toUpperCase' does not exist on type 'T'.Ограничение обещает только length. Вместо T может прийти массив, а у массива нет toUpperCase. Внутри функции доступно ровно то, что написано в ограничении. Если функции нужны методы строки, ей не нужен T: её параметр — обычный string.
Главное
T extends { length: number }разрешает подставить вместоTтолько тип со свойствомlength: строки и массивы подходят, числа — нет.- Внутри функции можно пользоваться тем, что обещает ограничение, и ничем больше.
- Аргумент, который не подходит под ограничение, tsc отмечает в строке вызова:
error TS2345. - В отличие от типа
{ length: number }у параметра,Tсохраняет точный тип результата.