Перейти к содержимому
Теория · Дженерики · урок 5 из 10

Ограничения extends

Иногда обобщённой функции мало знать, что T — какой-то тип: ей нужно что-то от самого значения. Напишем функцию longest, которая возвращает более длинное из двух значений — строк или массивов:

TypeScript
function longest<T>(a: T, b: T): T {
    return a.length >= b.length ? a : b; // ✗
}
TEXT
error TS2339: Property 'length' does not exist on type 'T'.

TypeScript прав. Вместо T можно подставить любой тип: number, boolean, даже null. У числа нет length, и функция сломалась бы на первом же таком вызове. Пока о T ничего не известно, TypeScript не даёт читать у значения свойства.

Вернёмся в порт. Кран поднимает любой контейнер, у которого на крыше есть проушины для крюков. Крану всё равно, что внутри: мебель или бананы. Но контейнер без проушин он не возьмёт. Функции longest нужно то же самое: груз любой, лишь бы у него было свойство length.

Ограничение через extends

Ограничение параметра типа записывают словом extends после его имени. Оно говорит, какие типы можно подставить вместо T:

TypeScript
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 с числом.

Что это даёт:

  1. Внутри функции TypeScript знает, что у a и b есть length, и ошибки больше нет.
  2. longest("cat", "horse") проходит: у строк есть length. Программа печатает horse.
  3. longest([1, 2], [3]) тоже проходит: у массивов есть length. Программа печатает 1 2.

Слово extends вы уже видели у классов. Здесь оно не про наследование: строка ничего не наследует от { length: number }, у неё есть нужное свойство, и этого достаточно. TypeScript сравнивает типы по форме, как с интерфейсами.

Аргумент без length

Теперь передадим числа:

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

console.log(longest(10, 200)); // ✗
TEXT
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() не скомпилируется:

TEXT
error TS2339: Property 'toUpperCase' does not exist on type '{ length: number; }'.

С T extends { length: number } TypeScript помнит, что пришли строки: у результата строковый тип, и toUpperCase() компилируется. Ограничение говорит, что значению нужно уметь, а T помнит, чем оно было.

На собеседовании часто спрашивают, что делает extends в записи <T extends …>. Ответ: он ограничивает, какие типы можно подставить вместо T, и разрешает пользоваться внутри функции тем, что ограничение обещает.

Частая ошибка

Внутри функции взяли у значения то, чего ограничение не обещает:

TypeScript
function shout<T extends { length: number }>(x: T): string {
    return x.toUpperCase(); // ✗
}
TEXT
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 сохраняет точный тип результата.