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

Проверка на null и сравнение

Вернёмся на сортировочную ленту. Иногда лоток доезжает до арки пустым. Прежде чем решать, конверт в нём или банка, работник смотрит, лежит ли в лотке хоть что-нибудь. Пустой лоток уходит в сторону, а дальше по ленте едут только настоящие посылки.

В программе пустой лоток — это null или undefined. Вы уже знаете, что у значения типа string | null нельзя вызвать метод строки, пока не исключён null:

TypeScript
function greet(nick: string | null) {
    console.log("Hi, " + nick.toUpperCase()); // ✗
}
TEXT
error TS18047: 'nick' is possibly 'null'.

Для null и undefined проверка typeof не нужна. TypeScript сужает тип и после двух других проверок: условия на истинность и сравнения.

Проверка на истинность

Условие if (nick) пропускает в ветку только истинные значения. null и undefined в JavaScript ложные, поэтому tsc убирает их из типа внутри if:

TypeScript
function greet(nick: string | null) {
    if (nick) {
        console.log("Hi, " + nick.toUpperCase());
    } else {
        console.log("Hi, guest");
    }
}

greet("ann"); // Hi, ANN
greet(null);  // Hi, guest
greet("");    // Hi, guest

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

  1. Внутри if у nick тип string: null сюда не попадает, и toUpperCase компилируется.
  2. greet("ann") печатает Hi, ANN.
  3. greet(null) уходит в else и печатает Hi, guest.
  4. greet("") тоже печатает Hi, guest: пустая строка в JavaScript ложная.

Последний вызов важен. В else у nick тип string | null, а не null: туда попадает и пустая строка. Условие на истинность отсекает все ложные значения JavaScript сразу:

  • null и undefined;
  • false;
  • 0, 0n и NaN;
  • пустую строку "".

Здесь аналогия с лотком перестаёт работать. Пустая строка и ноль — не пустой лоток, а настоящие посылки, но проверка на истинность отправляет их в сторону вместе с пустыми лотками.

Сравнение через = и !

Когда нужно отсечь только null, его сравнивают явно. TypeScript понимает и ===, и !==:

TypeScript
function badge(unread: number | null) {
    if (unread !== null) {
        return "unread: " + unread;
    }
    return "loading";
}

function same(a: string | number, b: string | boolean) {
    if (a === b) {
        return a.toUpperCase();
    }
    return "-";
}

console.log(badge(0), same("ok", "ok")); // unread: 0 OK

Программа печатает unread: 0 OK. Разберём обе функции.

Сравнение с null

unread !== null убирает из типа ровно null. Внутри if у unread тип number, и ноль остаётся настоящим числом, поэтому badge(0) возвращает unread: 0. После return у unread остался тип null.

С undefined пишут так же: x !== undefined. А вот typeof x === "object" для этой цели не годится: в JavaScript typeof null тоже даёт "object", и tsc об этом помнит, поэтому null в такой ветке останется.

Сравнение двух значений

Сравнивать можно и два значения с типами-объединениями. Если a === b истинно, в a и b лежит одно и то же значение, а значит, у него тип, который есть в обоих объединениях:

  • у a варианты string и number;
  • у b варианты string и boolean;
  • общий вариант у них один — string.

Поэтому внутри if tsc считает строками и a, и b, и a.toUpperCase() компилируется.

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

Проверили число на истинность, чтобы отсечь null:

TypeScript
function badge(unread: number | null) {
    if (unread) { // ✗
        return "unread: " + unread;
    }
    return "loading";
}

console.log(badge(0));

tsc молчит: внутри if у unread тип number, всё законно. Но программа печатает loading, хотя количество сообщений известно — оно равно нулю. Ноль ложный, и условие отправило его в ту же ветку, что и null. Когда 0 или пустая строка — нормальные значения, проверяйте unread !== null, как в примере выше. Эту ловушку любят проверять на собеседованиях.

Главное

  • if (x) убирает из типа null и undefined, но отсекает и другие ложные значения: 0, "", NaN, false.
  • x !== null убирает из типа ровно null, а ноль и пустая строка остаются в ветке.
  • Если a === b, внутри if у обоих значений тип, общий для их объединений.
  • tsc не предупредит, что if (unread) отправил ноль в ветку «значения нет»: это ошибка в логике, а не в типах.