Перегрузки: одна реализация, несколько сигнатур
Здесь будет встроен видео-разбор темы (5–7 минут, YouTube / RuTube / VK Видео) — тем, кто лучше воспринимает на слух, текст ниже не обязателен.
Развернуть можно и строку, и массив — тело функции получается почти одно, а вот тип результата разный: строке положена строка, массиву массив. Одной сигнатурой это не записывается, объединение string | string[] сваливает оба случая в кучу. Перегрузки дают функции несколько подписей при одном теле — и вместе с ними несколько новых способов соврать.
Разворачивать умеют оба типа: строке для этого нужен split, массиву хватает готового reverse. Логика одна, различается только ветка — и напрашивается одна функция на два случая. Тип для неё пишется единственным доступным пока способом: объединение на входе, объединение на выходе.
Тело безупречно: сужение по typeof отработало, обе ветки вернули что положено. Ругань идёт на седьмой строке, у вызывающего — Свойство "toUpperCase" не существует в типе "string | string[]", и вложенной причиной, что у string[] такого свойства нет. Знакомая матрёшка из статьи про чтение диагностики.
Формально компилятор прав: в подписи написано «на выходе строка или массив», а какое именно из двух — не сказано. Но мы-то передали строку, и это видно прямо в тексте вызова. Информация была — её просто негде записать: аннотация возврата не знает, с каким аргументом функцию позвали.
Лечится это на месте вызова, и в том вся неприятность.
Диагностик больше нет, но проверку придётся повторять на каждом вызове — включая те, где аргумент строковый литерал и сомневаться не в чем. Это не безопасность, а налог: там, где связь между входом и выходом потеряна в типе, её восстанавливают руками.
Возврат-объединение сам по себе не ошибка. Плохо, когда объединение шире того, что функция реально может вернуть при данном аргументе.
Перегрузка развязывает узел иначе: тело остаётся одно, а подписей у функции становится несколько. Пишутся они подряд — сначала сигнатуры без тела, каждая с точкой с запятой на конце, следом реализация.
Ни одной диагностики, а тело не изменилось ни на символ. Три строки, начинающиеся с function reverse, — это по-прежнему одна функция: первые две называются сигнатурами перегрузки и составляют её публичный интерфейс, третья — сигнатура реализации, и тело принадлежит ей.
На вызове компилятор идёт по списку сигнатур сверху вниз, берёт первую, куда подходят аргументы, и объявляет её тип возврата типом всего выражения. reverse("код") попал в первую — результат string, и toUpperCase разрешён без всяких проверок. reverse(["а", "б", "в"]) попал во вторую — результат string[], и у него есть join.
Сигнатуры и реализация должны идти вплотную. Вставьте между ними хоть одну строку — получите TS2391: «Реализация функции отсутствует либо не идет сразу после объявления».
Несколько function подряд — синтаксис объявлений функций и методов. Стрелке подписи приходится описывать типом: в объекте-типе разрешено перечислить сразу несколько сигнатур вызова, без имён и без function.
Проверить одно тело против двух сигнатур сразу компилятор не берётся, поэтому такое присваивание он отвергает целиком. Приходится явно отказаться от проверки возврата — и это ровно та цена, о которой пойдёт речь в разделе про обещания.
TS2394 сравнивает сигнатуры между собой — и только. Как тело распорядится аргументом, не проверяет никто. Отсюда самая дорогая ошибка этой статьи.
Компилятор промолчал заслуженно — по своим правилам. Он проверил, что string из первой сигнатуры укладывается в string | number из реализации, и что number из второй укладывается тоже. Всё сошлось. А утверждения «для ключа "theme" возвращается именно строка» в типах нет: обычная аннотация возврата не умеет зависеть от значения аргумента.
Значит, перегрузка — обещание, которое поддерживаете вы, а не компилятор. По уровню доверия она стоит рядом с as: и то и другое означает «я знаю лучше», и в обоих случаях цена ошибки — TypeError у пользователя вместо красной строки в редакторе.
Проверка тут ровно одна и ручная: тесты. Каждой сигнатуре — свой вызов; молчание компилятора его не заменит.
Практический вывод из этого простой: перегрузок должно быть мало и они должны быть скучными. Четыре сигнатуры над хитрым телом — почти гарантированная рассинхронизация через полгода, причём немая.
Мысль напрашивается: пусть компилятор сверит каждую ветку тела со «своей» сигнатурой. Но внутри тела перегрузок уже нет — там живёт только сигнатура реализации, и параметр в ней объединение. Убедиться легко тем же приёмом с намеренно неверным типом.
Диагностика называет "theme" | "fontSize" — тело видит объединение, а не два отдельных случая. Связать тип аргумента с типом результата в одной сигнатуре умеют условные типы, которым отведена девятая глава, но и они описывают, а не проверяют.
Перегрузка оправдана ровно тогда, когда тип результата зависит от типа аргумента. Если не зависит — она ничего не даёт и при этом отнимает.
Обе сигнатуры обещают number, так что выбор между ними вызывающему безразличен. Зато цена та же, что и раньше: аргумент-объединение больше не принимается, хотя тело с ним справилось бы. Уберите перегрузки — станет короче и мощнее одновременно.
Второй частый повод для лишней перегрузки — разное число аргументов. Здесь ответ дают необязательные параметры и значения по умолчанию из второй статьи этой главы: пара сигнатур ради одного greeting описывает то же самое втрое длиннее, а проверку числа аргументов компилятор и так делает сам.
| Что у вас | Чем описывать |
|---|---|
| Тип результата зависит от типа аргумента | перегрузка |
| Результат одинаков при любом варианте аргумента | объединение в параметре |
| Различается только число аргументов | необязательный параметр или значение по умолчанию |
Вызывающему нечем сузить: у него на руках 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 заработали без единой проверки типа на месте вызова. Тело менять нельзя.
Каждому ключу — своя сигнатура, а прежняя строка с объединением становится сигнатурой реализации. Обратите внимание, что параметры сигнатур — литеральные типы, а не string: именно они и позволяют компилятору выбрать нужную.
Сигнатуру реализации убирать нельзя: без неё тело останется без подписи. И наоборот, она не заменяет перегрузки — снаружи её не видно, так что вызвать readSetting с переменной типа "theme" | "fontSize" после этой правки уже не получится. Здесь это не мешает, ключи в вызовах литеральные.
Перегрузка, которой не нужно быть
Функция считает длину — и у строки, и у массива. Кто-то описал её двумя перегрузками, и теперь вызов из report не проходит: TS2769. Почините так, чтобы report заработал, а прежние вызовы со строкой и с массивом остались допустимыми.
Лечится удалением: обе сигнатуры перегрузки возвращали number, то есть не сообщали вызывающему ничего нового. Достаточно оставить бывшую сигнатуру реализации — она уже принимает и строку, и массив.
Проверить себя можно одним вопросом: отличаются ли типы возврата у сигнатур? Если нет, перегрузка только сужает круг допустимых аргументов и ничего не даёт взамен.
Тело нарушило обещание
На этот код компилятор не жалуется ни строчкой, а запуск падает с TypeError. Найдите, где тело разошлось с сигнатурами, и почините тело — сигнатуры трогать нельзя. Заодно объясните себе, почему TS2394 здесь не сработал.
Тело подкладывало в массив лишний разделитель, поэтому toArray(7) давало [7, "—"], а сигнатура обещала number[]. Убираем лишний элемент.
TS2394 молчал потому, что сравнивает только сигнатуры между собой: number[] прекрасно укладывается в (string | number)[], и на этом проверка заканчивается. Что тело реально положит в массив, в сравнении не участвует — обещание держите вы.