Перегрузки: одна реализация, несколько сигнатур

функции≈ 10 минут

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

Развернуть можно и строку, и массив — тело функции получается почти одно, а вот тип результата разный: строке положена строка, массиву массив. Одной сигнатурой это не записывается, объединение string | string[] сваливает оба случая в кучу. Перегрузки дают функции несколько подписей при одном теле — и вместе с ними несколько новых способов соврать.

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

Разворачивать умеют оба типа: строке для этого нужен split, массиву хватает готового reverse. Логика одна, различается только ветка — и напрашивается одна функция на два случая. Тип для неё пишется единственным доступным пока способом: объединение на входе, объединение на выходе.

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

Тело безупречно: сужение по typeof отработало, обе ветки вернули что положено. Ругань идёт на седьмой строке, у вызывающего — Свойство "toUpperCase" не существует в типе "string | string[]", и вложенной причиной, что у string[] такого свойства нет. Знакомая матрёшка из статьи про чтение диагностики.

Формально компилятор прав: в подписи написано «на выходе строка или массив», а какое именно из двух — не сказано. Но мы-то передали строку, и это видно прямо в тексте вызова. Информация была — её просто негде записать: аннотация возврата не знает, с каким аргументом функцию позвали.

Лечится это на месте вызова, и в том вся неприятность.

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

Диагностик больше нет, но проверку придётся повторять на каждом вызове — включая те, где аргумент строковый литерал и сомневаться не в чем. Это не безопасность, а налог: там, где связь между входом и выходом потеряна в типе, её восстанавливают руками.

Возврат-объединение сам по себе не ошибка. Плохо, когда объединение шире того, что функция реально может вернуть при данном аргументе.

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

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

Ни одной диагностики, а тело не изменилось ни на символ. Три строки, начинающиеся с function reverse, — это по-прежнему одна функция: первые две называются сигнатурами перегрузки и составляют её публичный интерфейс, третья — сигнатура реализации, и тело принадлежит ей.

На вызове компилятор идёт по списку сигнатур сверху вниз, берёт первую, куда подходят аргументы, и объявляет её тип возврата типом всего выражения. reverse("код") попал в первую — результат string, и toUpperCase разрешён без всяких проверок. reverse(["а", "б", "в"]) попал во вторую — результат string[], и у него есть join.

Сигнатуры и реализация должны идти вплотную. Вставьте между ними хоть одну строку — получите TS2391: «Реализация функции отсутствует либо не идет сразу после объявления».

Несколько function подряд — синтаксис объявлений функций и методов. Стрелке подписи приходится описывать типом: в объекте-типе разрешено перечислить сразу несколько сигнатур вызова, без имён и без function.

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

Проверить одно тело против двух сигнатур сразу компилятор не берётся, поэтому такое присваивание он отвергает целиком. Приходится явно отказаться от проверки возврата — и это ровно та цена, о которой пойдёт речь в разделе про обещания.

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

Сигнатура реализации в публичный список не входит. Её не видно совсем — и это заметно раньше, чем хотелось бы.

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

Аргумент типа string | string[] — дословно то, что написано в сигнатуре реализации, и тело для него и создано. Но перебираются только публичные сигнатуры, а объединение целиком не подходит ни к одной: строка не лезет в string[], массив не лезет в string.

Диагностика TS2769 устроена как отчёт о переборе: сверху вердикт «Ни одна перегрузка не соответствует этому вызову», ниже по пункту на каждую попытку — «Перегрузка 1 из 2, "(value: string): string", возвратила следующую ошибку» и причина отказа. Когда сигнатур много, полезнее всего именно нумерация: она сразу говорит, сколько вариантов вообще было.

Обратное требование тоже есть: реализация обязана принять всё, что обещают сигнатуры. Не примет — TS2394.

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

«Сигнатура перегрузки несовместима с ее сигнатурой реализации»: компилятор примерил boolean к string | string[] и не нашёл, куда его положить. Это единственная нить, связывающая список сигнатур с телом, — и в следующем разделе выяснится, насколько она тонкая.

Порядок сигнатур — не косметика. Побеждает первая подходящая, а не самая точная.

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

string[] подходит и под unknown[], поэтому перебор остановился на первой строке: "head" относится к типу unknown. Поменяйте две первые строки местами — и та же программа станет корректной. Правило короткое: от частного к общему.

Половина стандартной библиотеки описана перегрузками, просто без синтаксиса это не бросается в глаза. Пример из части I: reduce без начального значения и с ним ведут себя по-разному не из-за необязательного параметра, а потому что это разные сигнатуры.

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

Промах по количеству аргументов у перегруженной функции даёт не TS2769, а TS2554: «Ожидалось аргументов: 1-2, получено: 0». Сигнатуры сначала отсеиваются по числу параметров, и если не выжила ни одна, разбирать типы уже не о чем.

TS2394 сравнивает сигнатуры между собой — и только. Как тело распорядится аргументом, не проверяет никто. Отсюда самая дорогая ошибка этой статьи.

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

Компилятор промолчал заслуженно — по своим правилам. Он проверил, что string из первой сигнатуры укладывается в string | number из реализации, и что number из второй укладывается тоже. Всё сошлось. А утверждения «для ключа "theme" возвращается именно строка» в типах нет: обычная аннотация возврата не умеет зависеть от значения аргумента.

Значит, перегрузка — обещание, которое поддерживаете вы, а не компилятор. По уровню доверия она стоит рядом с as: и то и другое означает «я знаю лучше», и в обоих случаях цена ошибки — TypeError у пользователя вместо красной строки в редакторе.

Проверка тут ровно одна и ручная: тесты. Каждой сигнатуре — свой вызов; молчание компилятора его не заменит.

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

Мысль напрашивается: пусть компилятор сверит каждую ветку тела со «своей» сигнатурой. Но внутри тела перегрузок уже нет — там живёт только сигнатура реализации, и параметр в ней объединение. Убедиться легко тем же приёмом с намеренно неверным типом.

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

Диагностика называет "theme" | "fontSize" — тело видит объединение, а не два отдельных случая. Связать тип аргумента с типом результата в одной сигнатуре умеют условные типы, которым отведена девятая глава, но и они описывают, а не проверяют.

Перегрузка оправдана ровно тогда, когда тип результата зависит от типа аргумента. Если не зависит — она ничего не даёт и при этом отнимает.

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

Обе сигнатуры обещают number, так что выбор между ними вызывающему безразличен. Зато цена та же, что и раньше: аргумент-объединение больше не принимается, хотя тело с ним справилось бы. Уберите перегрузки — станет короче и мощнее одновременно.

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

Второй частый повод для лишней перегрузки — разное число аргументов. Здесь ответ дают необязательные параметры и значения по умолчанию из второй статьи этой главы: пара сигнатур ради одного greeting описывает то же самое втрое длиннее, а проверку числа аргументов компилятор и так делает сам.

ts
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера
Что у васЧем описывать
Тип результата зависит от типа аргументаперегрузка
Результат одинаков при любом варианте аргументаобъединение в параметре
Различается только число аргументовнеобязательный параметр или значение по умолчанию
Вызывающему нечем сузить: у него на руках string | numberобъединение — перегрузка такой аргумент не примет
Результат — «тот же тип, что пришёл»дженерик, о нём отдельная глава

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

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

  • Перегрузка — несколько публичных сигнатур при одном теле: сигнатуры пишутся подряд без тела, сразу за ними идёт реализация.
  • Компилятор перебирает сигнатуры сверху вниз и берёт первую подходящую, поэтому частное ставят выше общего.
  • Сигнатура реализации снаружи не видна: аргумент-объединение перегруженная функция не примет (TS2769), даже если тело с ним справляется.
  • Единственная связь списка с телом — TS2394, «сигнатура перегрузки несовместима с ее сигнатурой реализации»; что тело реально вернёт, не проверяет никто.
  • Поэтому перегрузка — обещание уровня as: соврали в ней — получите TypeError на исполнении при полностью молчаливом компиляторе.
  • Перегрузка нужна, только когда тип результата зависит от типа аргумента; иначе честнее объединение, необязательный параметр или дженерик.

1/4Функция объявлена тремя строками подряд: две начинаются с function reverse и заканчиваются точкой с запятой, третья — с телом. Сколько получилось функций и сколько у них способов вызова?

2/4У перегруженной reverse сигнатуры (value: string): string и (value: string[]): string[], а реализация принимает string | string[]. Что будет, если передать переменную типа string | string[]?

3/4Сигнатуры обещают для ключа "theme" тип string, реализация объявлена как (key: "theme" | "fontSize"): string | number, а тело для этого ключа возвращает число. Что скажет компилятор?

4/4Обе перегрузки функции возвращают number и различаются только типом параметра — string и unknown[]. Что даёт такая пара?

Настройка знает свой тип

Функция достаёт значение настройки: для "theme" — строку, для "fontSize" — число. Сейчас возврат объявлен объединением, и оба вызова упираются в диагностику: TS2339 и TS2365. Добавьте перегрузки так, чтобы обе строки с console.log заработали без единой проверки типа на месте вызова. Тело менять нельзя.

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

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

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

Сигнатуру реализации убирать нельзя: без неё тело останется без подписи. И наоборот, она не заменяет перегрузки — снаружи её не видно, так что вызвать readSetting с переменной типа "theme" | "fontSize" после этой правки уже не получится. Здесь это не мешает, ключи в вызовах литеральные.

Перегрузка, которой не нужно быть

Функция считает длину — и у строки, и у массива. Кто-то описал её двумя перегрузками, и теперь вызов из report не проходит: TS2769. Почините так, чтобы report заработал, а прежние вызовы со строкой и с массивом остались допустимыми.

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

Лечится удалением: обе сигнатуры перегрузки возвращали number, то есть не сообщали вызывающему ничего нового. Достаточно оставить бывшую сигнатуру реализации — она уже принимает и строку, и массив.

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

Проверить себя можно одним вопросом: отличаются ли типы возврата у сигнатур? Если нет, перегрузка только сужает круг допустимых аргументов и ничего не даёт взамен.

Тело нарушило обещание

На этот код компилятор не жалуется ни строчкой, а запуск падает с TypeError. Найдите, где тело разошлось с сигнатурами, и почините тело — сигнатуры трогать нельзя. Заодно объясните себе, почему TS2394 здесь не сработал.

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

Тело подкладывало в массив лишний разделитель, поэтому toArray(7) давало [7, "—"], а сигнатура обещала number[]. Убираем лишний элемент.

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

TS2394 молчал потому, что сравнивает только сигнатуры между собой: number[] прекрасно укладывается в (string | number)[], и на этом проверка заканчивается. Что тело реально положит в массив, в сравнении не участвует — обещание держите вы.