Тип функции: параметры, возврат, алиас для колбэка
Здесь будет встроен видео-разбор темы (5–7 минут, YouTube / RuTube / VK Видео) — тем, кто лучше воспринимает на слух, текст ниже не обязателен.
Функция — такое же значение, как число или строка, а значит, у неё есть свой тип. Записывается он стрелкой: (a: number, b: number) => number — слева параметры, справа то, что вернётся. Стоит дать этому типу имя, и колбэк перестаёт быть дырой, через которую в сигнатуры затекает any.
Типы параметров компилятор придумать не может: угадывать, что вы собирались передать, неоткуда. Их пишете вы. А вот тип возврата он выведет сам, прочитав тело.
Ни одной диагностики, и наведение на area покажет (width: number, height: number) => number: тип результата вывелся из умножения. Раз выводится — значит, писать его необязательно. Но иногда стоит.
Явная аннотация возврата проверяет не тех, кто функцию вызывает, а само тело. Обещание записано в сигнатуре, и нарушить его молча уже нельзя.
«Тип "number" не может быть назначен для типа "string"» — и указана строка с return. Теперь уберём аннотацию и посмотрим, куда переедет та же ошибка.
Диагностика та же самая, но приехала она на четвёртую строку — туда, где результат используют, а не туда, где его сочинили. В большом проекте это будет другой файл и другой автор. А дальше начинается второе действие: предупреждение никого не остановило, код выполнился, и label оказался числом 3. У чисел нет toUpperCase — в консоли настоящий TypeError.
Внутри модуля тип возврата обычно не пишут — вывод справляется. Пишут там, где сигнатура является обещанием для других: экспорт, публичный метод, чужой колбэк.
У числа тип number, у массива чисел — number[]. У функции тип записывается почти так же, как сама стрелочная функция: скобки с параметрами, стрелка, результат. Разница одна — справа от стрелки стоит не тело, а тип.
Первое, что стоит заметить: у 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; | обе сразу | слева описание, справа реализация |
Второе, что удивляет: имена параметров внутри типа ни к чему не обязывают. Они стоят там для чтения, как подпись на схеме, — сверяются только позиции и типы.
Ни звука. Distance говорит «два числа на входе, число на выходе», а как реализация назовёт свои параметры — её личное дело. Результат 7, а не −7: первый аргумент попал в первый параметр, что бы ни было написано в типе. Имя в типе — это комментарий, и проверять его нечем.
Рука привычно ставит двоеточие перед типом результата — так пишется возврат у обычной функции. В типе функции результат отделяется стрелкой.
«Ожидалось "=>"» — TS1005, ошибка разбора, а не типов. Двоеточие в этой позиции законно только внутри фигурных скобок, где оно означает запись метода; про неё — в конце статьи.
Колбэк из части I — функция, переданная аргументом. Её тип можно написать прямо в сигнатуре, но уже во второй такой сигнатуре скобки начинают мешать читать. Псевдоним типа — тот же type, что и для объектов, — чинит это одной строкой.
Formatter — обычное имя для типа, никакой особой конструкции для колбэков в языке нет. Колбэк написан прямо в аргументе, поэтому value снова получил тип по контексту: компилятор посмотрел, чего ждёт printAll, и раздал типы.
А теперь вынесем колбэк в переменную — так делают, когда его хочется переиспользовать или просто назвать.
«Параметр "value" неявно имеет тип "any"» — и указана строка с short, а не строка с вызовом. Переменная объявлена сама по себе, взять тип параметра неоткуда: контекст появится позже, при передаче, а типизируется функция в момент объявления. Обратите внимание, на самой передаче диагностики нет — (value: any) => string подходит куда угодно, за то any и не любят.
Лечится это не аннотацией параметра, а аннотацией переменной.
Это и есть главный приём статьи: тип колбэка называют один раз, а дальше на имя ссылаются — и в сигнатуре функции, и на переменной. Ни одного any и ни одного повторённого : number.
Колбэк, который ничего не возвращает, описывают как «… => void». У void в этой позиции есть отдельная особенность — ей посвящена третья статья главы.
Проверка совместимости функций несимметрична, и с непривычки правило кажется вывернутым: функция с меньшим числом параметров подходит туда, где объявлено больше. Наоборот — не подходит.
Правило становится очевидным, если встать на место вызывающего кода. Он даст ровно два аргумента: brief лишний просто не возьмёт — это безопасно, а verbose попросит третий, и взять его негде. Диагностика говорит именно это: «Целевая подпись предоставляет слишком мало аргументов. Ожидал 3 или больше, но получил 2». Ниже, в выводе, видно и последствие — undefined.
На этом правиле держится половина повседневного кода. map зовёт колбэк с тремя аргументами — значением, индексом и всем массивом, — а берём мы обычно один.
Обратная сторона того же правила — знаменитая ловушка, на которой компилятор промолчит.
Первая строка вывода — [10, NaN, 2]. У parseInt два параметра: строка и система счисления. Два меньше трёх, форма сошлась, компилятор доволен — а map тем временем подставляет индекс вторым аргументом, и он оказывается основанием: parseInt("10", 1) даёт NaN, parseInt("10", 2) — двойку. Проверка следит за формой, а не за замыслом; вторая строка с явным колбэком берёт ровно то, что нужно.
Компилятор сверяет формы, а не намерения. Типы снимают целый класс ошибок, но не заменяют чтение документации к чужой функции.
Фабрики функций из главы про замыкания в типах выглядят непривычно ровно один раз. Справа от стрелки может стоять что угодно — в том числе ещё одна стрелка.
Читается такой тип слева направо: принимает k, отдаёт функцию, которая принимает x и отдаёт число. Скобки вокруг второй стрелки не нужны — всё, что справа, целиком считается типом результата. Внутренняя функция замкнула k, как и в части I; типы к этому механизму ничего не добавили, кроме проверки.
Одного взгляда на Multiplier хватает, чтобы понять, что multiplier(2) — это ещё не число, а функция, и её нужно вызвать второй раз. Ради такой читаемости стрелочные типы и выносят в псевдонимы.
Стрелочная запись описывает только вызов и больше ничего. Но функция в JavaScript — объект, и на неё можно навесить свойства: счётчик обращений, кеш, настройку. Обычный тип функции такого не разрешит.
«Свойство "language" не существует в типе "(name: string) => string"». Чтобы описать и вызов, и свойства, берут объектный тип, а вызов записывают внутри него отдельной строкой — это и есть сигнатура вызова.
Внутри фигурных скобок двоеточие снова законно — та самая позиция, где на нём спотыкались в начале статьи. Так же, кстати, записывают методы объекта: 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 на месте вызова.
Тип сравнения выносим в псевдоним и ставим его параметру. Дальше контекстная типизация раздаст типы a и b сама.
Одна аннотация на параметре погасила сразу три TS7006: у колбэка появился контекст. Имя Comparator теперь можно поставить и на переменную, если сравнение захочется вынести отдельно.
Обещание, которое не сдержали
Псевдоним обещает строку, а реализация отдаёт число. Компилятор об этом скажет, но код всё равно выполнится — и вы увидите, чем именно плохо игнорировать диагностику. Почините реализацию так, чтобы percent(0.256) дал ...26%.
Диагностика TS2322 указывает на третью строку: Math.round возвращает число, а Formatter требует строку. На исполнении это превращается в TypeError: percent(...).padStart is not a function — у числа нет строковых методов.
Обратите внимание, где сработала защита: не на вызове, а на присваивании. Тип на переменной проверяет реализацию в момент её написания — это ровно тот эффект, ради которого пишут аннотацию возврата.
Обёртка с логом
withLog принимает функцию и возвращает такую же, но с печатью аргумента. Сейчас неявный any в трёх местах. Опишите одним псевдонимом тип «число на входе, число на выходе» и расставьте его так, чтобы не пришлось аннотировать ни x, ни n.
Один псевдоним занимает обе позиции сразу: и параметр, и возврат. После этого x получает тип из аннотации возврата, а n — из параметра при вызове.
Тип NumberFn здесь жёстко привязан к числам: для строк придётся заводить второй такой же. Способ написать обёртку один раз для любых типов — дженерики, им отведена седьмая глава.