Перейти к содержимому
Теория · Функции · урок 7 из 10

Колбэки и контекстная типизация

Во второй теме параметр без типа был ошибкой TS7006: выводу нечего читать. А в JavaScript колбэки пишут без всяких типов, и в TypeScript такой код тоже компилируется:

TypeScript
const prices = [100, 250];
const total = prices.map((p) => p + 300);
console.log(total.join(" ")); // 400 550

У p нет аннотации, но и ошибки нет. Наведите курсор на p: подсказка покажет (parameter) p: number. Откуда TypeScript это узнал?

Вспомните табличку автомата. У map тоже есть табличка — её тип. На ней написано, что map сделает с вашей функцией: вызовет её для каждого элемента и передаст ей элемент, его номер и сам массив. Когда стрелка стоит прямо в вызове prices.map(...), TypeScript читает эту табличку и сам подписывает параметры. p — элемент массива чисел, значит number.

Тип из места вызова

Контекстная типизация — так называют вывод типа из места, где стоит функция. Обычный вывод идёт от значения к имени: из let count = 5 получается number. Контекстный идёт в обратную сторону — от места к функции:

  • стрелка передана в map массива number[] — её параметр получает number;
  • стрелку кладут в переменную с типом функции — параметры берут типы из этого типа;
  • стрелка передана в функцию с параметром-функцией — типы приходят из сигнатуры этой функции.

Поэтому колбэкам, которые пишут прямо в вызове, аннотации обычно не нужны. Тип результата при этом выводится как всегда — из тела: p + 300 даёт number, и total получает тип number[].

Нажимайте «Следующий шаг» и следите, откуда каждая стрелка берёт тип параметра — и где ей взять его неоткуда.

ВживуюОткуда колбэк знает типышаг 1 из 8

TypeScript вывел из литерала массив чисел.

Следите, откуда стрелка берёт тип параметра

Меньше параметров — можно

map передаёт колбэку три аргумента, а стрелка (p) => p + 300 принимает один. Функция с меньшим числом параметров подходит к типу функции с большим: лишние аргументы она не читает, и JavaScript это разрешает. Так работают почти все колбэки: номер элемента и сам массив нужны редко.

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

TypeScript
type Visit = (x: number, i: number) => void;

const both: Visit = (x: number, i: number, extra: number) => { // ✗
    console.log(x, i, extra);
};
TEXT
error TS2322: Type '(x: number, i: number, extra: number) => void' is not assignable to type 'Visit'.
  Target signature provides too few arguments. Expected 3 or more, but got 2.

Вторая строка сообщения объясняет причину: «тип, к которому присваивают, даёт слишком мало аргументов — нужно 3 или больше, а есть 2». Тот, кто вызывает Visit, передаст только x и i, и в extra всегда был бы undefined.

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

Частая ошибка

Колбэк вынесли из вызова в константу, чтобы дать ему имя:

TypeScript
const prices = [100, 250];
const addFee = (p) => p + 300; // ✗
console.log(prices.map(addFee).join(" "));
TEXT
error TS7006: Parameter 'p' implicitly has an 'any' type.

Здесь аналогия с табличкой заканчивается. Человек может прочитать табличку заранее и запомнить, а TypeScript читает табличку map, только когда стрелка стоит прямо в вызове map. То, что addFee строкой ниже передаётся в map, тип p не подсказывает. Вынося колбэк, пишите тип параметра сами — или дайте константе тип функции, как const log: Visit выше:

TypeScript
const prices = [100, 250];
const addFee = (p: number) => p + 300;
console.log(prices.map(addFee).join(" ")); // 400 550

Главное

  • Контекстная типизация берёт типы параметров из места, где стоит функция: из вызова map, из типа переменной, из сигнатуры функции, которой её передают.
  • Стрелке прямо в вызове аннотации не нужны, а вынесенная в константу стрелка без типов даёт ошибку TS7006.
  • Функция с меньшим числом параметров подходит к типу функции с большим; с большим — не подходит.
  • Колбэк, который выносят из вызова, получает типы параметров явно: (p: number) => p + 300.