Обобщённые функции
Чему вы научитесь:
- писать обобщённые функции, которые сохраняют точный тип значения;
- понимать, как TypeScript выводит параметр типа
Tиз аргументов каждого вызова; - ограничивать параметр типа через
extends; - описывать обобщённые типы и классы и читать
Array<T>,Map<K, V>иSet<T>.

Представьте морской порт. Все контейнеры в нём одинаковые: стальные стенки, двери, замок. В один грузят мебель, в другой — бананы, в третий — книги. Конструкция одна на любой груз, а чтобы было понятно, что внутри, на двери есть рамка для этикетки. Её заполняют при погрузке, и порт на другом конце света знает, что внутри, не открывая двери.
Функции бывают такими же контейнерами. Функция first возвращает первый элемент массива, и ей всё равно, что лежит в массиве. В JavaScript её пишут один раз. Как описать её типы в TypeScript?
Первое, что приходит в голову, — тип any. Функция одна и принимает любой массив, но этикетку с контейнера сорвали:
function first(xs: any[]): any {
return xs[0];
}
const price = first([100, 250]);
console.log(price.toUpperCase()); // ✗TypeError: price.toUpperCase is not a functiontsc молчит: у price тип any, и проверка выключена. Ошибку увидит только тот, кто запустит программу. Второй путь — по копии функции на каждый тип: firstNumber, firstString и так далее. Проверка работает, но одно и то же тело приходится писать снова и снова.
Параметр типа T
Обобщённая функция объявляет в угловых скобках после имени параметр типа — имя вроде T, вместо которого при каждом вызове подставляется настоящий тип. Это и есть рамка для этикетки. Не путайте с формулировкой из сообщений tsc: там «параметр типа number» значит «параметр, у которого тип number». Здесь параметр типа — это сам T:
function first<T>(xs: T[]): T | undefined {
return xs[0];
}
const price = first([100, 250]);
const city = first(["Paris", "Rome"]);
console.log(price, city); // 100 ParisРазберём объявление по частям:
<T>после имени объявляет параметр типаT;xs: T[]— параметр функции: массив, все элементы которого типаT;T | undefined— тип результата: элемент того же типаT, а у пустого массиваundefined.
Давайте посмотрим, что происходит при вызовах:
first([100, 250])получает массив чисел, и TypeScript заполняет рамку:Tстановитсяnumber. Подсказка редактора уpriceпокажетnumber | undefined.first(["Paris", "Rome"])получает массив строк, и здесьT— этоstring. Уcityтипstring | undefined.console.logпечатает100 Paris.
Функция одна, а тип результата у каждого вызова свой. И проверка снова на месте:
function first<T>(xs: T[]): T | undefined {
return xs[0];
}
const price = first([100, 250]);
console.log(price?.toUpperCase()); // ✗error TS2339: Property 'toUpperCase' does not exist on type 'number'.tsc знает, что в price число или undefined, и находит ошибку до запуска.
На собеседовании часто спрашивают, зачем нужны дженерики и чем они лучше any. Ответ: обобщённая функция работает с любым типом и при этом связывает тип входа с типом результата, а any эту связь рвёт и выключает проверку.
Явный аргумент типа
Тип, который подставляют вместо T, называют аргументом типа. Это тоже не «аргумент типа string» из сообщений tsc, то есть не аргумент, у которого тип string. Обычно TypeScript находит его сам, но его можно написать и явно — в угловых скобках при вызове: first<number>([100, 250]). Тогда TypeScript не выводит T, а проверяет аргументы по заданному типу. Вызов first<number>([1, "2"]) не скомпилируется:
error TS2322: Type 'string' is not assignable to type 'number'.Это как этикетка, которую написали до погрузки: груз, который ей не соответствует, в контейнер не пустят.
Здесь аналогия с портом заканчивается. В порту этикетка едет вместе с контейнером, а T, как и все типы, существует только для tsc. В JavaScript-файле от функции останется function first(xs), без угловых скобок.
Частая ошибка
Внутри обобщённой функции вернули значение конкретного типа:
function firstOr<T>(xs: T[]): T {
if (xs.length === 0) {
return 0; // ✗
}
return xs[0];
}error TS2322: Type 'number' is not assignable to type 'T'.
'T' could be instantiated with an arbitrary type which could be unrelated to 'number'.Вторая строка сообщения объясняет причину: вместо T могут подставить любой тип, не связанный с number. T выбирает тот, кто вызывает функцию, а не сама функция. Вызов firstOr(["Paris"]) ждёт строку, и 0 обманул бы его. Это всё равно что заранее положить в контейнер бананы, хотя этикетку заполнит отправитель.
Запасное значение передают параметром того же типа: function firstOr<T>(xs: T[], fallback: T): T. Тогда его выбирает вызывающий код, и тип совпадает.
Главное
- Обобщённая функция объявляет параметр типа
<T>, и при каждом вызове вместоTподставляется настоящий тип. first<T>(xs: T[]): T | undefinedсвязывает тип элементов с типом результата: для массива чисел результат —number | undefined.anyтоже принимает любой массив, но теряет тип результата и выключает проверку.- Аргумент типа можно написать явно,
first<number>(…), и тогда TypeScript проверяет аргументы по нему.