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

Обобщённые функции

Чему вы научитесь

Чему вы научитесь:

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

Представьте морской порт. Все контейнеры в нём одинаковые: стальные стенки, двери, замок. В один грузят мебель, в другой — бананы, в третий — книги. Конструкция одна на любой груз, а чтобы было понятно, что внутри, на двери есть рамка для этикетки. Её заполняют при погрузке, и порт на другом конце света знает, что внутри, не открывая двери.

Функции бывают такими же контейнерами. Функция first возвращает первый элемент массива, и ей всё равно, что лежит в массиве. В JavaScript её пишут один раз. Как описать её типы в TypeScript?

Первое, что приходит в голову, — тип any. Функция одна и принимает любой массив, но этикетку с контейнера сорвали:

TypeScript
function first(xs: any[]): any {
    return xs[0];
}

const price = first([100, 250]);
console.log(price.toUpperCase()); // ✗
TEXT
TypeError: price.toUpperCase is not a function

tsc молчит: у price тип any, и проверка выключена. Ошибку увидит только тот, кто запустит программу. Второй путь — по копии функции на каждый тип: firstNumber, firstString и так далее. Проверка работает, но одно и то же тело приходится писать снова и снова.

Параметр типа T

Обобщённая функция объявляет в угловых скобках после имени параметр типа — имя вроде T, вместо которого при каждом вызове подставляется настоящий тип. Это и есть рамка для этикетки. Не путайте с формулировкой из сообщений tsc: там «параметр типа number» значит «параметр, у которого тип number». Здесь параметр типа — это сам T:

TypeScript
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.

Давайте посмотрим, что происходит при вызовах:

  1. first([100, 250]) получает массив чисел, и TypeScript заполняет рамку: T становится number. Подсказка редактора у price покажет number | undefined.
  2. first(["Paris", "Rome"]) получает массив строк, и здесь T — это string. У city тип string | undefined.
  3. console.log печатает 100 Paris.

Функция одна, а тип результата у каждого вызова свой. И проверка снова на месте:

TypeScript
function first<T>(xs: T[]): T | undefined {
    return xs[0];
}

const price = first([100, 250]);
console.log(price?.toUpperCase()); // ✗
TEXT
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"]) не скомпилируется:

TEXT
error TS2322: Type 'string' is not assignable to type 'number'.

Это как этикетка, которую написали до погрузки: груз, который ей не соответствует, в контейнер не пустят.

Здесь аналогия с портом заканчивается. В порту этикетка едет вместе с контейнером, а T, как и все типы, существует только для tsc. В JavaScript-файле от функции останется function first(xs), без угловых скобок.

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

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

TypeScript
function firstOr<T>(xs: T[]): T {
    if (xs.length === 0) {
        return 0; // ✗
    }
    return xs[0];
}
TEXT
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 проверяет аргументы по нему.