Дженерик: тип как параметр

дженерики≈ 10 минут

Здесь будет встроен видео-разбор темы (5–7 минут, YouTube / RuTube / VK Видео) — тем, кто лучше воспринимает на слух, текст ниже не обязателен.

Функция, которая отдаёт то же, что получила, — обычное дело: первый элемент массива, значение из кеша, аргумент после проверки. А записать это обещание в типах нечем: any связь между входом и выходом стирает, объединение размывает. Дженерик добавляет функции второй вид параметров — параметр типа: он подставляется в момент вызова и держит связь.

Разделы темы:·

У функции уже есть параметры — в круглых скобках, для значений. Дженерик добавляет вторые, в угловых, — для типов. T здесь не какой-то особенный тип, а имя-заполнитель: ровно как list — имя-заполнитель для массива, который придёт при вызове.

ts
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

Одна функция, два вызова, две разные проверки. В первом на место T встал string — toUpperCase разрешён. Во втором T оказался number: «Свойство "toUpperCase" не существует в типе "number"», и указана седьмая строка — место ошибки, а не функция, которая ни в чём не виновата. Дальше по традиции: диагностика выполнение не отменяет, РИКО успевает напечататься до падения.

Сравните с самым первым примером: код тот же, изменились две аннотации — any[] стал T[], any стал T. Одна буква, повторённая дважды, и есть связь «что вошло, то и вышло».

Параметр значенияПараметр типа
Где объявленв круглых скобках: function f(list)в угловых: function f<T>()
Что подставляютзначение: f([1, 2])тип: f<number>([1, 2])
Когда подставляютпри вызове, во время работы программыпри вызове, во время проверки типов
Что остаётся в JavaScriptаргументничего, угловые скобки стираются

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

Подставить тип можно и руками — теми же угловыми скобками, только уже при вызове.

ts
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

Здесь мы объявили T строкой, а принесли числа — и компилятор сверил аргумент с обещанием: «Тип "number" не может быть назначен для типа "string"». Диагностик две, по одной на каждый элемент массива.

В живом коде угловые скобки при вызове почти не пишут: компилятор выводит T из аргументов сам. Как именно — следующая статья главы.

number[] из второй главы — сокращённая запись. Полная выглядит так, что теперь её понятно читать.

ts
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

Array<number> и number[] — один и тот же тип, две записи. У Map параметров типа два, Map<K, V> — ключ и значение; мы подставили string и number, и четвёртая строка попалась: «Аргумент типа "string" нельзя назначить параметру типа "number"». Никакой отдельной поддержки Map внутри компилятора нет — работает тот самый механизм, который мы только что написали руками.

Промисы из части I — тоже дженерик: Promise<T>, где T — тип того, чем промис разрешится.

ts
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

await снимает обёртку и отдаёт string — поэтому toUpperCase на месте. А седьмая строка показывает, что параметр типа участвует в сравнении наравне со всем прочим: «Тип "Promise<string>" не может быть назначен для типа "Promise<number>"», и вложенной причиной — про string и number.

Почти всё, что хранит чужие значения: Set<T>, Map<K, V>, WeakMap<K, V>, Promise<T>, Array<T> и его неизменяемая версия ReadonlyArray<T>. Знакомый по главе про индексные сигнатуры Record<K, V> — оттуда же.

ts
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

«Аргумент типа "number" нельзя назначить параметру типа "string"»: Set запомнил, что в нём строки, и стережёт это на каждом add.

«Вернуть то же самое» — частный случай. Параметр типа можно поставить в любое место сигнатуры, и связь опишется любая.

ts
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

Вход — одно значение типа T, выход — массив T[]. У repeat(true, 3) результат boolean[], у repeat("ок", 2) — string[], и join у него есть. А последняя строка ловится: toUpperCase у boolean нет. Обратите внимание, что times остался обычным number — не всё в сигнатуре обязано быть параметром типа.

Параметров типа тоже может быть несколько — перечисляются через запятую, как обычные.

ts
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

A схватил string, B — number, а результат описан кортежем [A, B] — тем самым кортежем из второй главы, где важен порядок. Поэтому у row[0] есть toUpperCase, у row[1] — toFixed, и перепутать их компилятор не даст.

Имена A и B выбраны для читаемости: две буквы подряд лучше видно, чем T и U. Компилятору всё равно, как их зовут.

Параметр типа не ограничен примитивами: T — это любой тип, включая тот, который вы описали сами.

ts
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

Последняя строка — «Свойство "email" не существует в типе "User"». T стал User целиком, со всеми полями и со всей строгостью, которая к User прилагается.

Соблазн читать T как «вежливый any» велик. Проверим его на функции, которая просто выкрикивает переданное.

ts
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

TS2339, «Свойство "toUpperCase" не существует в типе "T"» — тот же код, что и на опечатке в имени поля, только в типе теперь стоит буква. И это при том, что вызвали мы shout со строкой и РИКО в консоли напечаталось. Тело функции проверяется один раз, до всех вызовов, и обязано быть верным для любой подстановки. Раз T может оказаться числом, toUpperCase под запретом.

Вот здесь и проходит граница между any и T. any разрешает внутри всё и не обещает наружу ничего. T наоборот: внутри почти ничего нельзя, зато наружу дано твёрдое обещание. Сказать компилятору «T — не совсем что угодно, а хотя бы что-то со строчными методами» можно, для этого есть слово extends, и ему отведена третья статья главы.

Из того же правила следует второе ограничение: значение типа T нельзя сочинить — его можно только получить снаружи.

ts
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

Диагностика с вложенной причиной, читается как разбор из первой главы: «Тип "number" не может быть назначен для типа "T"», а ниже почему — «Возможно создание экземпляра "T" с произвольным типом, который может быть не связан с "number"». По-человечески: вы обещали вернуть тот тип, который выберет вызывающий, а вернули ноль. Пятая и шестая строки это и доказывают: T выбрали string, из функции пришёл 0, и toUpperCase падает уже при выполнении.

Наконец, T — обычное имя, и живёт оно только внутри своей функции.

ts
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

«Не удается найти имя "T"»: снаружи параметра типа не существует — ровно как не существует снаружи имени параметра функции. Буква T ничем не выделена, её просто принято брать первой.

ИмяОткудаГде встречается
TTypeединственный параметр типа
U, Vследующие буквы алфавитавторой и третий параметры
K, VKey, Valueпары ключ-значение, как в Map<K, V>
EElementэлемент коллекции
Item, Valueобычное словокогда буква перестаёт читаться

Однобуквенные имена — традиция, а не правило. Когда параметров типа три и они разной природы, слово читается лучше буквы.

  • any в сигнатуре не описывает связь входа и выхода, а отменяет её вместе со всеми проверками.
  • Дженерик — это параметр типа: имя в угловых скобках, на место которого при вызове встаёт настоящий тип.
  • Одна буква T, повторённая в параметре и в возврате, и есть обещание «что вошло, то и вышло».
  • Параметров типа бывает несколько, и стоять они могут где угодно в сигнатуре: T на входе, T[] или [A, B] на выходе.
  • Array<T>, Map<K, V>, Promise<T> — тот же механизм: дженериками вы пользовались задолго до этой статьи.
  • Внутри функции про T не известно почти ничего: тело проверяется один раз и обязано подойти любой будущей подстановке.

1/4Функция объявлена как function firstItem(list: any[]): any, а в коде написано firstItem([31, 42]).toUpperCase(). Что скажет компилятор?

2/4Есть function pair<A, B>(first: A, second: B): [A, B] и вызов pair("Рико", 31). Какой тип у второго элемента результата?

3/4Внутри function shout<T>(value: T) написали value.toUpperCase(), а вызвали shout("Рико"). Что будет?

4/4Чем Array<number> отличается от number[]?

Уберите any

Функция возвращает последний элемент массива, и обе аннотации у неё any. Из-за этого ошибка в седьмой строке видна только в браузере. Перепишите сигнатуру на параметр типа так, чтобы компилятор поймал её до запуска — тело менять не нужно.

ts
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

Объявляем параметр типа T в угловых скобках и ставим его на оба места, где стоял any: массив на входе — T[], результат — T.

tsxлистинг · не запускается
Листинг только для чтения: этот код не запускается в браузере.

Первый вызов работает как раньше — T стал string. Во втором T — это number, и седьмая строка теперь подсвечена в редакторе, а не падает у пользователя.

Пара наоборот

swap меняет местами элементы пары. С any компилятор согласен на любую глупость: в четвёртой строке пришли строка и число, а после перестановки к числу применяют toUpperCase — и никакой диагностики. Опишите функцию двумя параметрами типа так, чтобы результат был кортежем в обратном порядке.

ts
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

Два параметра типа, A и B, и кортеж на выходе с переставленными местами: [B, A].

jsлистинг · не запускается
Листинг только для чтения: этот код не запускается в браузере.

Теперь swapped[0] — число, а swapped[1] — строка, и swapped[0].toUpperCase() перестаёт компилироваться. Тип возврата пришлось написать руками: сам факт перестановки внутри тела — не то, о чём компилятор догадается.

Значение по умолчанию

Функция должна вернуть первый элемент, а для пустого массива — что-нибудь запасное. Компилятор возражает на третью строку. Прочитайте диагностику вместе с вложенной причиной, объясните её себе и почините функцию, не выбрасывая T.

ts
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

Причина в диагностике сказана прямо: «Возможно создание экземпляра "T" с произвольным типом, который может быть не связан с "number"». Вызывающий выбирает T сам, и ноль подойдёт только если он выбрал числа. Сочинить значение типа T внутри функции невозможно — его нужно получить снаружи, вторым параметром.

tsxлистинг · не запускается
Листинг только для чтения: этот код не запускается в браузере.

Одна буква T теперь связывает три места: элементы массива, запасное значение и результат. Передать строку с запасным нулём компилятор не даст.