Сужение через typeof
Чему вы научитесь:
- понимать, как проверка
typeofменяет тип переменной в каждой ветке и послеreturn; - проверять значения на
null, сравнивать их и различать объекты черезinиinstanceof; - описывать варианты размеченным объединением и ловить забытый вариант через
never; - писать свои функции-проверки с предикатом
x is T.

Представьте сортировочную ленту на почте. По ней вперемешку едут плоские конверты и круглые жестяные банки. Пока посылка на общей ленте, работник знает о ней только одно: это конверт или банка. Конверт вскрывают ножом для бумаги, банку — консервным ножом, и браться за инструмент наугад нельзя.
Поэтому посреди ленты стоит арка. Она смотрит на каждую посылку и отправляет её в свой жёлоб: конверты налево, банки направо. В жёлобе с конвертами любой работник точно знает, что перед ним конверт, и сразу берёт нужный нож.
В программе общая лента — это значение типа-объединения. Количество товара в корзине приходит из двух мест: из поля формы — строкой вроде " 4 ", из кода — числом 3. Функция toCount должна вернуть число в обоих случаях:
function toCount(v: string | number): number {
return Number(v.trim()); // ✗
}error TS2339: Property 'trim' does not exist on type 'string | number'.
Property 'trim' does not exist on type 'number'.Метод trim есть только у строки, а в v может прийти число. Если бы этот JavaScript запустили с числом, программа упала бы на этой строке. Нужна та самая арка: проверка, после которой понятно, что именно лежит в v.
Проверка typeof
Такую проверку вы знаете из JavaScript: оператор typeof возвращает строку с названием вида значения. TypeScript читает это условие и внутри каждой ветки даёт переменной свой тип:
function toCount(v: string | number): number {
if (typeof v === "number") {
return v;
} else {
return Number(v.trim());
}
}
console.log(toCount(3)); // 3
console.log(toCount(" 4 ")); // 4Давайте разберём пример по строкам:
typeof v === "number"— обычная проверка JavaScript. При запуске она сравнивает строку, которую вернулtypeof, со словом"number".- Внутри
ifуvтипnumber: сюда попадают только числа, поэтомуreturn vподходит к результатуnumber. - В
elseостался один вариант —string, и вызовv.trim()компилируется. - Программа печатает
3и4.
Когда компилятор по проверке в коде оставляет у переменной меньше вариантов, чем в её объявлении, это называют сужением типа. Проверка typeof — та самая арка на ленте, а ветки if и else — два жёлоба.
Сама проверка останется в JavaScript-файле и сработает при запуске. А сужение существует только для tsc: это заключение, которое он делает по вашему коду, и в .js от него ничего не остаётся.
Сужение после return
Ветку else можно не писать. Если ветка с числом заканчивается return, с числом функция завершится прямо в этой ветке. До строк после if доберётся только строка — и TypeScript это видит: сужение действует до конца функции.
Пройдите функцию по шагам и следите, какой тип подсказка показывает на каждой выделенной строке:
1function toCount(v: string | number) {подсказка редактораv: string | number2 if (typeof v === "number") {3 return v;4 }5 const text = v.trim();6 return Number(text);7}8 9const a = toCount(3);10const b = toCount(" 4 ");11console.log(a + b);
На входе v может быть и строкой, и числом.
В этой версии нет ни аннотации результата, ни else. На строке 5 у v тип string: число до неё не доходит, потому что его ветка закончилась return. Тип результата TypeScript тоже вывел сам: обе ветки возвращают number.
На собеседовании часто спрашивают, что такое сужение типов. Ответ: компилятор следит за проверками в коде — typeof, сравнениями, return — и в каждом месте программы даёт переменной тот тип, который там возможен.
Что видит typeof
Здесь аналогия с аркой перестаёт работать. Арка разглядывает саму посылку и может заметить что угодно. typeof видит значение уже при запуске, а типов TypeScript к этому времени нет: они стёрты при компиляции. Поэтому typeof возвращает одно из восьми слов JavaScript, и имени вашего типа среди них не бывает:
type User = { nick: string };
function nickOf(u: User | string) {
if (typeof u === "User") { // ✗
return u.nick;
}
return u;
}error TS2367: This comparison appears to be unintentional because the types '"bigint" | "boolean" | "function" | "number" | "object" | "string" | "symbol" | "undefined"' and '"User"' have no overlap.tsc перечисляет все восемь слов, которые может вернуть typeof, и сообщает, что "User" ни с одним из них не совпадает. Такая проверка была бы ложной при любом значении. Обычный объект typeof называет словом "object", поэтому здесь подойдёт typeof u === "object": внутри такого if у u тип User.
Частая ошибка
В ветке с числом забыли return:
function show(v: string | number) {
if (typeof v === "number") {
console.log(v.toFixed(1));
}
console.log(v.trim()); // ✗
}error TS2339: Property 'trim' does not exist on type 'string | number'.
Property 'trim' does not exist on type 'number'.Ветка с числом не заканчивается return, поэтому после if выполнение продолжится и с числом. Для tsc у v там снова полный тип string | number, и trim он не пропускает. Если бы этот JavaScript запустили с числом, программа упала бы с TypeError: v.trim is not a function. Исправить можно двумя способами: закончить ветку return или перенести строку с trim в else.
Главное
- Проверка
typeof v === "number"сужает тип: внутриifуvтипnumber, а вelse— оставшийсяstring. - Если ветка заканчивается
return, сужение действует до конца функции: послеifостаётся толькоstring. - Без
returnи безelseпослеifу переменной снова полный тип-объединение. typeofзнает только восемь видов значений JavaScript: имя типа из TypeScript он проверить не может.