Перейти к содержимому
Теория · Сужение типов · урок 1 из 12

Сужение через typeof

Чему вы научитесь

Чему вы научитесь:

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

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

Поэтому посреди ленты стоит арка. Она смотрит на каждую посылку и отправляет её в свой жёлоб: конверты налево, банки направо. В жёлобе с конвертами любой работник точно знает, что перед ним конверт, и сразу берёт нужный нож.

В программе общая лента — это значение типа-объединения. Количество товара в корзине приходит из двух мест: из поля формы — строкой вроде " 4 ", из кода — числом 3. Функция toCount должна вернуть число в обоих случаях:

TypeScript
function toCount(v: string | number): number {
    return Number(v.trim()); // ✗
}
TEXT
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 читает это условие и внутри каждой ветки даёт переменной свой тип:

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

Давайте разберём пример по строкам:

  1. typeof v === "number" — обычная проверка JavaScript. При запуске она сравнивает строку, которую вернул typeof, со словом "number".
  2. Внутри if у v тип number: сюда попадают только числа, поэтому return v подходит к результату number.
  3. В else остался один вариант — string, и вызов v.trim() компилируется.
  4. Программа печатает 3 и 4.

Когда компилятор по проверке в коде оставляет у переменной меньше вариантов, чем в её объявлении, это называют сужением типа. Проверка typeof — та самая арка на ленте, а ветки if и else — два жёлоба.

Сама проверка останется в JavaScript-файле и сработает при запуске. А сужение существует только для tsc: это заключение, которое он делает по вашему коду, и в .js от него ничего не остаётся.

Сужение после return

Ветку else можно не писать. Если ветка с числом заканчивается return, с числом функция завершится прямо в этой ветке. До строк после if доберётся только строка — и TypeScript это видит: сужение действует до конца функции.

Пройдите функцию по шагам и следите, какой тип подсказка показывает на каждой выделенной строке:

ВживуюЧто видит TypeScriptшаг 1 из 5

На входе v может быть и строкой, и числом.

Нажимайте «Следующий шаг»: подсказка покажет тип выделенного имени

В этой версии нет ни аннотации результата, ни else. На строке 5 у v тип string: число до неё не доходит, потому что его ветка закончилась return. Тип результата TypeScript тоже вывел сам: обе ветки возвращают number.

На собеседовании часто спрашивают, что такое сужение типов. Ответ: компилятор следит за проверками в коде — typeof, сравнениями, return — и в каждом месте программы даёт переменной тот тип, который там возможен.

Что видит typeof

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

TypeScript
type User = { nick: string };

function nickOf(u: User | string) {
    if (typeof u === "User") { // ✗
        return u.nick;
    }
    return u;
}
TEXT
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:

TypeScript
function show(v: string | number) {
    if (typeof v === "number") {
        console.log(v.toFixed(1));
    }
    console.log(v.trim()); // ✗
}
TEXT
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 он проверить не может.