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

Свои проверки типа

Сложную проверку на сортировочной ленте выносят в отдельную будку. Работник будки вскрывает посылку, смотрит, что внутри, и ставит штамп «курьер». Дальше посылку никто не вскрывает: её направляют по штампу. Лента верит штампу на слово.

В программе такая будка — функция-проверка. Проверку "phone" in d из урока про in удобно вынести в функцию: правило записано один раз, а вызвать его можно в любом месте программы. Но если объявить, что функция возвращает boolean, сужение пропадёт:

TypeScript
type Courier = { phone: string };
type Pickup = { address: string };

function isCourier(d: Courier | Pickup): boolean {
    return "phone" in d;
}

function contact(d: Courier | Pickup) {
    if (isCourier(d)) {
        return "call " + d.phone; // ✗
    }
    return "pick up";
}
TEXT
error TS2339: Property 'phone' does not exist on type 'Courier | Pickup'.
  Property 'phone' does not exist on type 'Pickup'.

Для tsc результат isCourier(d) — обычное true или false. Из типа boolean не видно, что true значит «это курьер», поэтому в ветке у d остаётся всё объединение.

Предикат типа

Чтобы функция сужала тип, вместо boolean пишут предикат типа — запись d is Courier. Она читается так: «если функция вернула true, то d — это Courier»:

TypeScript
type Courier = { phone: string };
type Pickup = { address: string };

function isCourier(d: Courier | Pickup): d is Courier {
    return "phone" in d;
}

function contact(d: Courier | Pickup) {
    if (isCourier(d)) {
        return "call " + d.phone;
    }
    return "pick up at " + d.address;
}

console.log(contact({ address: "Main St 5" })); // pick up at Main St 5

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

  1. d is Courier стоит на месте типа результата. При запуске функция возвращает обычный true или false.
  2. Слева от is — имя параметра функции: предикат говорит именно о d.
  3. Внутри if (isCourier(d)) у d тип Courier, как после проверки "phone" in d.
  4. После return остался Pickup: предикат сужает и ветку, где функция вернула false.
  5. Программа печатает pick up at Main St 5.

Это и есть штамп из будки. Функция один раз смотрит на объект, а if в любом месте программы сужает тип по её ответу.

Здесь аналогия со штампом перестаёт работать. Штамп остаётся на посылке, а предикат ничего не оставляет на объекте. После компиляции от d is Courier остаётся обычная функция, которая возвращает true или false.

Когда TypeScript выводит предикат сам

С версии 5.5 TypeScript умеет выводить предикат сам. Уберите : boolean из первого примера — и подсказка редактора над isCourier покажет function isCourier(d: Courier | Pickup): d is Courier, а сужение заработает.

Так получается не всегда. Предикат выводится для функции без типа результата, в которой один return и возвращается сама проверка. Если в теле несколько return, например if ("phone" in d) { return true; } return false;, тип результата снова boolean. В таких случаях, а также когда обещание хочется видеть в объявлении функции, предикат пишут явно.

На собеседовании об этом часто спрашивают: как написать свою функцию, которая сужает тип? Ответ: объявить её результат предикатом x is T.

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

В теле функции-проверки перепутали ключ:

TypeScript
type Courier = { phone: string };
type Pickup = { address: string };

function isCourier(d: Courier | Pickup): d is Courier {
    return "address" in d; // ✗
}

function contact(d: Courier | Pickup) {
    if (isCourier(d)) {
        return "call " + d.phone.trim();
    }
    return "pick up at " + d.address;
}

console.log(contact({ address: "Main St 5" }));

tsc молчит: он не сверяет тело функции с её предикатом. Функция возвращает true для самовывоза, а tsc верит предикату и считает d курьером. Поэтому он пропускает d.phone.trim(), хотя у самовывоза телефона нет. При запуске программа падает:

TEXT
TypeError: Cannot read properties of undefined (reading 'trim')

Предикат — обещание, и проверять его приходится вам, а не компилятору. Неверное тело сужает тип неверно, и ошибка всплывает только при запуске.

Главное

  • Функция с типом результата boolean не сужает тип там, где её вызвали.
  • Предикат d is Courier вместо boolean сужает тип: при true до Courier, при false до остальных вариантов.
  • TypeScript 5.5 и новее сам выводит предикат, если у функции нет типа результата, а её единственный return возвращает саму проверку.
  • tsc верит предикату на слово: неверное тело сузит тип неверно, и ошибка всплывёт при запуске.