Необязательные параметры, значения по умолчанию и rest

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

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

Одному вызову нужны все аргументы, другому хватит первого — и в JavaScript пропущенный аргумент молча становится undefined. TypeScript требует объявить такую вольность заранее: знаком ?, значением по умолчанию или ...rest. Взамен он считает аргументы за вас и не даёт забыть проверку — а заодно вводит правило, из-за которого необязательный параметр нельзя поставить перед обязательным.

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

В части I пропуск аргумента был обычным делом: функция получала undefined и как-то с этим жила. Компилятор смотрит иначе — он считает аргументы и не разрешает вызвать функцию не так, как она объявлена.

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

«Ожидалось аргументов: 2, получено: 1» — это TS2554. Обратите внимание, что исполнение всё равно состоялось и напечатало undefined Рико: диагностика ничего не останавливает. Из чистого JavaScript ровно такая строка и уехала бы в прод.

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

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

Счёт аргументов сошёлся, зато появилась диагностика внутри функции: TS18048, «Возможно, title имеет значение undefined». Дальше классический сюжет: предупреждение проигнорировали, первый вызов отработал, второй упал с TypeError — у undefined нет метода toUpperCase. Компилятор показал ровно ту строку, на которой программа и сломалась.

Знак вопроса не делает параметр волшебным, он добавляет к типу undefined. Внутри функции title — это string | undefined, то есть обычное объединение, которое сначала сужают.

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

Приём с заведомо неподходящим типом знаком по статье про сужение, но здесь он показывает не одну переменную, а всю сигнатуру: (name: string, title?: string | undefined) => string. Знак вопроса и | undefined стоят рядом не случайно — это две стороны одной записи. Снаружи аргумент можно не передавать, внутри его придётся проверить.

Знак вопроса ставится только в объявлении. На месте вызова не меняется ничего — вы просто не пишете аргумент.

Лишний аргумент в JavaScript молча пропадает. Компилятор считает и сверху тоже.

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

«Ожидалось аргументов: 1-2, получено: 3». Диапазон в тексте диагностики — это и есть подпись функции словами: один обязательный параметр, второй необязательный. Исполнение при этом напечатало инженер Рико, третий аргумент просто некуда было деть.

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

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

Внутри функции — ни одной диагностики: toUpperCase вызван без всякой проверки, и это законно. Единственная претензия компилятора — намеренная ложь про тип в последней строке, и она же называет сигнатуру целиком: (name: string, title?: string) => string. Знак вопроса на месте, а | undefined исчез: снаружи параметр по-прежнему можно пропустить, но внутрь undefined не доберётся, его перехватит умолчание. Третий вызов показывает, что явный undefined считается за «не передали» — это одно из немногих мест, где undefined и null ведут себя по-разному не на словах.

Тип параметра компилятор берёт из самого значения по умолчанию, аннотацию писать не нужно. Ошибиться типом при вызове всё равно не дадут.

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

Второй вызов — тот случай, ради которого стоит читать диагностику до запуска. «Аргумент типа string нельзя назначить параметру типа number» — а на исполнении ни ошибки, ни падения: пустая строка. repeat честно превратил "три" в NaN, округлил до нуля и повторил текст ноль раз. Такую пропажу в интерфейсе ищут долго.

У вывода типа из умолчания есть обратная сторона: выводит он широко.

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

Диагностик нет. Из "быстрый" компилятор вывел не литеральный тип, а string — как и для обычной let-переменной в статье про литеральные типы. Умолчание задаёт значение, а не список допустимых значений. Если вариантов должно быть ровно два, тип нужно назвать.

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

Аннотация и умолчание уживаются: level: Level = "info" читается как «тип вот такой, а если не передали — вот это». В тексте диагностики параметр назван Level | undefined: вариант «не передали» компилятор держит в уме и здесь, просто внутрь функции он не проходит.

И ещё одно свойство, общее с JavaScript: умолчание — это выражение. Оно вычисляется при каждом вызове и видит параметры, объявленные левее.

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

Вопросительный знак и умолчание вместе не ставят: это TS1015, «Параметр не может содержать вопросительный знак и инициализатор». Умолчание и так делает параметр необязательным.

Вывод типа из умолчания подводит там, где само умолчание ничего не значит.

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

«Аргумент типа "*" нельзя назначить параметру типа null | undefined» — из null компилятору выводить нечего, и тип параметра получился ровно null. Функция, в которую нельзя передать ничего осмысленного, — не то, что задумывали. Лечится аннотацией filler: string | null = null или нормальным умолчанием вроде filler = " ".

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

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

TS1016, «Обязательный параметр не должен следовать за необязательным», и заметьте — диагностика на первой строке, а не на вызове. Запрещено само объявление. Причина видна, если попробовать таким объявлением воспользоваться: раз title можно пропустить, вызов order("Рико") должен что-то значить. Но единственный аргумент по позиции попадёт в title, а обязательный name останется пустым. Правило снимает неоднозначность до того, как она появится.

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

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

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

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

TS1016 не сработал — и это не поблажка. Сигнатура из последней строки объясняет всё: (title: string | undefined, name: string) => string, без всякого знака вопроса. Параметр с умолчанием, стоящий перед обязательным, перестаёт быть пропускаемым: аргумент передавать обязательно, просто разрешено передать undefined. Правило про порядок не нарушено, потому что необязательных параметров здесь попросту нет.

Третий вызов подтверждает это дважды. Компилятор требует два аргумента — «Ожидалось аргументов: 2, получено: 1». Исполнение печатает Рико undefined: строка «Рико» ушла в title, а name не досталось ничего. Вывод практический — значение по умолчанию ставьте в конец списка, иначе оно приносит не удобство, а обязанность писать undefined при каждом вызове.

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

Когда необязательных параметров становится больше двух, позиции перестают работать вовсе: вызов вида find("привет", undefined, undefined, true) прочитать невозможно. Тогда их собирают в один объект с необязательными полями — теми самыми, из статьи про типы объектов.

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

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

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

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

Тип number[] описывает не один аргумент, а весь хвост: каждый аргумент должен быть числом, а сколько их — неважно, ноль тоже подходит. Третий вызов напечатал 12, и это не опечатка: reduce сложил ноль с единицей, получил 1, встретил строку — и сложение стало склейкой. То самое преобразование типов из части I, от которого диагностика и защищает.

Тип rest-параметра обязан быть массивом. Забытые квадратные скобки компилятор ловит отдельным кодом.

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

«Параметр rest должен иметь тип массива» — TS2370. Вторая диагностика, на вызове, — эхо первой, и в ней виден любопытный след: список аргументов компилятор описал кортежем [number, number]. Именно так он его и представляет.

Rest-параметр всегда последний, и это правило не TypeScript, а самого языка.

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

Диагностик две: TS1014, «Параметр rest должен стоять на последнем месте в списке параметров», и следом обычная путаница со счётом аргументов. А исполнения не будет вообще — движок отказывается разбирать такой код целиком и бросает SyntaxError: Rest parameter must be last formal parameter. Редкий случай, когда компилятор и среда исполнения ругаются на одно и то же: три точки не в конце запрещены самим синтаксисом языка.

На стороне вызова три точки работают в обратную сторону — раскладывают массив в отдельные аргументы. В rest-параметр это проходит всегда, в обычные — не всегда.

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

«Аргумент расширения должен иметь тип кортежа либо передаваться в параметр rest» — TS2556. У sizes тип number[], массив неизвестной длины, и компилятор не может обещать, что там ровно два числа. У fixed благодаря as const тип readonly [3, 4]: длина известна, вызов разрешён. На исполнении оба варианта дали 12 — но только один из них будет давать его всегда.

В rest-параметре можно написать не массив, а кортеж — тогда получится функция с фиксированным числом аргументов, у которых есть имена.

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

«Ожидалось аргументов: 2-3, получено: 1» — знак вопроса внутри кортежа работает так же, как у параметра. Имена x, y и label на исполнение не влияют никак, они живут только в типе, зато попадают в подсказку редактора. Приём выглядит экзотикой, но именно на нём держатся типы вроде «те же аргументы, что у той функции», которые ждут в главе про дженерики.

Раз ? добавляет к типу undefined, кажется, что title?: string и title: string | undefined — одно и то же. Внутри функции да. Снаружи нет.

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

«Ожидалось аргументов: 1, получено: 0» — и это единственная диагностика примера. Функция b обязана получить аргумент, просто этим аргументом разрешено быть undefined. Разница не в типе значения, а в праве промолчать: вызывающий код должен явно сказать «здесь ничего нет», и забыть об этом параметре у него не выйдет.

ЗаписьАргумент можно пропуститьТип внутри функции
title?: stringдаstring | undefined
title = "гость"даstring
title: string | undefinedнет, undefined передают явноstring | undefined
...titles: string[]да, хоть ноль аргументовstring[]

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

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

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

У opt получилось два: знак вопроса стёрся вместе с остальными типами, и до браузера доехала функция с двумя обычными параметрами. А = "x" и ...r — настоящий синтаксис JavaScript, он никуда не девается, и length считает параметры только до него. Хорошее напоминание, что ? живёт исключительно на этапе проверки.

  • Знак вопроса разрешает не передавать аргумент и добавляет к типу параметра undefined: внутри функции его придётся сузить, иначе будет TS18048.
  • Значение по умолчанию делает параметр необязательным снаружи, но внутри оставляет чистый тип без undefined — проверять нечего. Явный undefined в вызове тоже включает умолчание.
  • Тип из умолчания выводится широко: из "быстрый" получается string, а не литерал. Нужен ограниченный набор значений — пишите аннотацию.
  • TS1016 запрещает обязательный параметр после необязательного, потому что аргументы позиционные. Параметр с умолчанием под это правило не подпадает — и заодно теряет право быть пропущенным.
  • Тип rest-параметра описывает весь хвост, поэтому обязан быть массивом или кортежем (TS2370), а сам параметр — стоять последним (TS1014).
  • Раскладывать массив в обычные параметры компилятор не даёт (TS2556): нужна известная длина, то есть кортеж. В rest-параметр раскладывается что угодно.

1/4Объявление function order(title = "гость", name: string) — что скажет компилятор?

2/4В функции function greet(name: string, title?: string) какой тип у title внутри тела?

3/4В function sum(...nums: number) забыли квадратные скобки. Что будет?

4/4В function slot(mode = "быстрый") пройдёт ли вызов slot("бытсрый") с опечаткой?

Скидка, которой может не быть

Функция считает цену со скидкой в процентах. Скидка есть не всегда, поэтому параметр сделали необязательным — и получили диагностику внутри функции и NaN на исполнении. Почините так, чтобы вызов без скидки возвращал полную цену, а сигнатура снаружи не изменилась.

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

Диагностика TS18048 говорит прямо: discount может оказаться undefined, а арифметика с undefined даёт NaN. Самое короткое лечение — заменить знак вопроса значением по умолчанию.

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

Ни диагностики, ни NaN: внутри функции discount имеет тип number, а снаружи параметр остался необязательным — вызов price(1000) как работал, так и работает. Вариант со знаком вопроса и const rate = discount ?? 0; даёт тот же результат, но лишней строкой: умолчание делает ровно это, только в объявлении, где его видно вызывающему коду.

Путь из частей

Функция должна склеивать путь из корня и любого числа сегментов. Автор ошибся в типе rest-параметра — и одна опечатка дала четыре диагностики. Исправьте тип и заодно вызов без сегментов: сейчас он возвращает home/ с лишней косой чертой.

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

Диагностик четыре, но ошибка одна: TS2370 на объявлении. Всё остальное — её последствия. Раз тип параметра объявлен как string, у него нет метода join (TS2339), а списки аргументов обоих вызовов внезапно не подходят под string (два раза TS2345). Чинится первая — исчезают все.

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

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

Предскажите три строки

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

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

Диагностика ровно одна, на шестой строке: «Ожидалось аргументов: 2, получено: 1». Объявление законно — TS1016 сюда не приходит, потому что параметр с умолчанием необязательным не считается. Вывод: id-7, №7, 7undefined.

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

Третья строка и есть тот самый здравый смысл наоборот: кажется, что единственный аргумент должен попасть в обязательный параметр, а умолчание — подставиться. Но аргументы позиционные, и «7» ушло в prefix. Умолчание перед обязательным параметром бесполезно — переставьте его вправо: function label(code: string, prefix = "№").