Тип функции: параметры, возврат, алиас для колбэка

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

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

Функция — такое же значение, как число или строка, а значит, у неё есть свой тип. Записывается он стрелкой: (a: number, b: number) => number — слева параметры, справа то, что вернётся. Стоит дать этому типу имя, и колбэк перестаёт быть дырой, через которую в сигнатуры затекает any.

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

Типы параметров компилятор придумать не может: угадывать, что вы собирались передать, неоткуда. Их пишете вы. А вот тип возврата он выведет сам, прочитав тело.

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

Ни одной диагностики, и наведение на area покажет (width: number, height: number) => number: тип результата вывелся из умножения. Раз выводится — значит, писать его необязательно. Но иногда стоит.

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

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

«Тип "number" не может быть назначен для типа "string"» — и указана строка с return. Теперь уберём аннотацию и посмотрим, куда переедет та же ошибка.

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

Диагностика та же самая, но приехала она на четвёртую строку — туда, где результат используют, а не туда, где его сочинили. В большом проекте это будет другой файл и другой автор. А дальше начинается второе действие: предупреждение никого не остановило, код выполнился, и label оказался числом 3. У чисел нет toUpperCase — в консоли настоящий TypeError.

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

У числа тип number, у массива чисел — number[]. У функции тип записывается почти так же, как сама стрелочная функция: скобки с параметрами, стрелка, результат. Разница одна — справа от стрелки стоит не тело, а тип.

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

Первое, что стоит заметить: у a и b нет аннотаций, а знакомой по второй главе TS7006 нет. Когда компилятор заранее знает, какого типа должна быть функция, он раздаёт типы её параметрам сам — это называется контекстной типизацией. Дописывать (a: number, b: number) не нужно: это было бы дублированием того, что уже сказано слева.

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

ЗаписьГде стрелкаЧто она означает
let f: (a: number) => number;в типеописание: принимает число, отдаёт число
const f = (a: number) => a * 2;в значениисама функция, её тело
const f: (a: number) => number = (a) => a * 2;обе сразуслева описание, справа реализация

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

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

Ни звука. Distance говорит «два числа на входе, число на выходе», а как реализация назовёт свои параметры — её личное дело. Результат 7, а не −7: первый аргумент попал в первый параметр, что бы ни было написано в типе. Имя в типе — это комментарий, и проверять его нечем.

Рука привычно ставит двоеточие перед типом результата — так пишется возврат у обычной функции. В типе функции результат отделяется стрелкой.

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

«Ожидалось "=>"» — TS1005, ошибка разбора, а не типов. Двоеточие в этой позиции законно только внутри фигурных скобок, где оно означает запись метода; про неё — в конце статьи.

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

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

Formatter — обычное имя для типа, никакой особой конструкции для колбэков в языке нет. Колбэк написан прямо в аргументе, поэтому value снова получил тип по контексту: компилятор посмотрел, чего ждёт printAll, и раздал типы.

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

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

«Параметр "value" неявно имеет тип "any"» — и указана строка с short, а не строка с вызовом. Переменная объявлена сама по себе, взять тип параметра неоткуда: контекст появится позже, при передаче, а типизируется функция в момент объявления. Обратите внимание, на самой передаче диагностики нет — (value: any) => string подходит куда угодно, за то any и не любят.

Лечится это не аннотацией параметра, а аннотацией переменной.

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

Это и есть главный приём статьи: тип колбэка называют один раз, а дальше на имя ссылаются — и в сигнатуре функции, и на переменной. Ни одного any и ни одного повторённого : number.

Колбэк, который ничего не возвращает, описывают как «… => void». У void в этой позиции есть отдельная особенность — ей посвящена третья статья главы.

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

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

Правило становится очевидным, если встать на место вызывающего кода. Он даст ровно два аргумента: brief лишний просто не возьмёт — это безопасно, а verbose попросит третий, и взять его негде. Диагностика говорит именно это: «Целевая подпись предоставляет слишком мало аргументов. Ожидал 3 или больше, но получил 2». Ниже, в выводе, видно и последствие — undefined.

На этом правиле держится половина повседневного кода. map зовёт колбэк с тремя аргументами — значением, индексом и всем массивом, — а берём мы обычно один.

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

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

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

Первая строка вывода — [10, NaN, 2]. У parseInt два параметра: строка и система счисления. Два меньше трёх, форма сошлась, компилятор доволен — а map тем временем подставляет индекс вторым аргументом, и он оказывается основанием: parseInt("10", 1) даёт NaN, parseInt("10", 2) — двойку. Проверка следит за формой, а не за замыслом; вторая строка с явным колбэком берёт ровно то, что нужно.

Компилятор сверяет формы, а не намерения. Типы снимают целый класс ошибок, но не заменяют чтение документации к чужой функции.

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

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

Читается такой тип слева направо: принимает k, отдаёт функцию, которая принимает x и отдаёт число. Скобки вокруг второй стрелки не нужны — всё, что справа, целиком считается типом результата. Внутренняя функция замкнула k, как и в части I; типы к этому механизму ничего не добавили, кроме проверки.

Одного взгляда на Multiplier хватает, чтобы понять, что multiplier(2) — это ещё не число, а функция, и её нужно вызвать второй раз. Ради такой читаемости стрелочные типы и выносят в псевдонимы.

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

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

«Свойство "language" не существует в типе "(name: string) => string"». Чтобы описать и вызов, и свойства, берут объектный тип, а вызов записывают внутри него отдельной строкой — это и есть сигнатура вызова.

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

Внутри фигурных скобок двоеточие снова законно — та самая позиция, где на нём спотыкались в начале статьи. Так же, кстати, записывают методы объекта: find(id: number): string и find: (id: number) => string в типе описывают почти одно и то же.

  • Типы параметров пишете вы, тип возврата компилятор выводит сам; явная аннотация возврата ловит ошибку в теле функции, а не в месте использования.
  • Тип функции записывается стрелкой: (a: number, b: number) => number. Имена параметров в нём декоративны — сверяются позиции и типы.
  • Контекстная типизация раздаёт типы параметрам, когда функция пишется прямо в аргументе или присваивается переменной с типом; у отдельного объявления контекста нет, и будет TS7006.
  • Псевдоним для колбэка — обычный type: тип называют один раз и ссылаются на имя и в сигнатуре, и на переменной.
  • Функция с меньшим числом параметров подходит туда, где ждут больше; наоборот — нет. На этом держится map с одним аргументом и ловушка map(parseInt).
  • Справа от стрелки может стоять другая стрелка — так описывают фабрики функций; если у функции есть ещё и свойства, нужен объектный тип с сигнатурой вызова.

1/4Объявлен type Listener = (index: number, total: number) => string, а присваивают ему const brief: Listener = (index) => "элемент " + index. Что скажет компилятор?

2/4Есть type Distance = (from: number, to: number) => number и реализация const walk: Distance = (to, from) => to - from. Что напечатает walk(10, 3)?

3/4Колбэк вынесен в отдельную переменную const short = (value) => "≈" + Math.round(value) и передан в функцию, которая ждёт параметр типа Formatter. Где будет диагностика?

4/4Функция без аннотации возврата возвращает число, а её результат кладут в const label: string, после чего вызывают label.toUpperCase(). Что произойдёт?

Тип для сравнения

Функция sortBy принимает массив и колбэк сравнения, но параметр compare остался без типа — и утянул за собой в any ещё и параметры самого колбэка. Опишите тип сравнения отдельным псевдонимом и уберите все три диагностики, не аннотируя a и b на месте вызова.

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

Тип сравнения выносим в псевдоним и ставим его параметру. Дальше контекстная типизация раздаст типы a и b сама.

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

Одна аннотация на параметре погасила сразу три TS7006: у колбэка появился контекст. Имя Comparator теперь можно поставить и на переменную, если сравнение захочется вынести отдельно.

Обещание, которое не сдержали

Псевдоним обещает строку, а реализация отдаёт число. Компилятор об этом скажет, но код всё равно выполнится — и вы увидите, чем именно плохо игнорировать диагностику. Почините реализацию так, чтобы percent(0.256) дал ...26%.

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

Диагностика TS2322 указывает на третью строку: Math.round возвращает число, а Formatter требует строку. На исполнении это превращается в TypeError: percent(...).padStart is not a function — у числа нет строковых методов.

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

Обратите внимание, где сработала защита: не на вызове, а на присваивании. Тип на переменной проверяет реализацию в момент её написания — это ровно тот эффект, ради которого пишут аннотацию возврата.

Обёртка с логом

withLog принимает функцию и возвращает такую же, но с печатью аргумента. Сейчас неявный any в трёх местах. Опишите одним псевдонимом тип «число на входе, число на выходе» и расставьте его так, чтобы не пришлось аннотировать ни x, ни n.

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

Один псевдоним занимает обе позиции сразу: и параметр, и возврат. После этого x получает тип из аннотации возврата, а n — из параметра при вызове.

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

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