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

Проверки in и instanceof

На сортировочную ленту приехали коробки. Снаружи они одинаковые, картонные, и арка typeof про любую из них скажет одно: «коробка». Чтобы разобрать коробки дальше, нужны другие приметы. Например, есть ли у коробки ручка для переноски или стоит ли на ней клеймо завода, который её сделал.

С объектами в JavaScript так же: для обычного объекта, массива или даты typeof возвращает "object". Служба доставки хранит заказы двух видов: с курьером — у такого заказа есть телефон, и самовывоз — у него есть адрес пункта выдачи. Проверка typeof их не различает:

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

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

Условие typeof d === "object" истинно для обоих видов заказа, поэтому tsc ничего не сужает: в ветке по-прежнему Courier | Pickup. Нужна проверка, которая смотрит на сам объект.

Оператор in

Оператор in из JavaScript проверяет при запуске, есть ли у объекта свойство с таким именем. tsc читает ту же проверку и оставляет в ветке только те варианты объединения, у которых это свойство объявлено:

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

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

console.log(contact({ phone: "555-01" }));      // call 555-01
console.log(contact({ address: "Main St 5" })); // pick up at Main St 5

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

  1. "phone" in d при запуске спрашивает у объекта, есть ли в нём ключ phone.
  2. Внутри if у d тип Courier: свойство phone объявлено только у него.
  3. После return остался Pickup, и d.address компилируется: сужение работает в обе стороны.
  4. Программа печатает call 555-01 и pick up at Main St 5.

Это та самая ручка на коробке: проверка смотрит, есть ли у объекта нужная деталь. Классы для in не нужны: он работает с обычными объектными типами, описанными через type.

instanceof и Array.isArray

Другая примета — клеймо завода. Объекты, созданные через new, проверяют оператором instanceof. Он узнаёт, каким конструктором сделан объект: Date, Error или классом JavaScript, который вы написали сами. Массив проверяют функцией Array.isArray:

TypeScript
function year(d: Date | string) {
    if (d instanceof Date) {
        return d.getFullYear();
    }
    return Number(d.slice(0, 4));
}

function count(tags: string | string[]) {
    if (Array.isArray(tags)) {
        return tags.length;
    }
    return 1;
}

console.log(year(new Date(2024, 4, 1)), count(["a", "b"])); // 2024 2

Программа печатает 2024 2. Разберём каждую проверку.

instanceof

Дата приходит объектом Date или строкой вроде "2025-01-31". Внутри if (d instanceof Date) у d тип Date, и метод getFullYear доступен. После return осталась строка, и d.slice компилируется. Вызов year(new Date(2024, 4, 1)) возвращает 2024.

Так же проверяют ошибку e, пойманную в catch: после e instanceof Error у e внутри ветки тип Error, и можно читать e.message.

Array.isArray

Теги приходят одной строкой или массивом строк. Array.isArray(tags) возвращает true только для массива, и tsc оставляет в ветке тип string[]. Вызов count(["a", "b"]) возвращает 2, а одна строка считается одним тегом.

Для массива typeof тоже даёт "object", поэтому здесь нужна именно Array.isArray.

in или instanceof

Этот вопрос часто задают на собеседовании. Коротко разница такая:

  • in проверяет, есть ли у объекта свойство с таким именем, и подходит для любых объектов, в том числе описанных через type;
  • instanceof проверяет, каким конструктором создан объект, и работает только с классами.

Здесь аналогия с клеймом перестаёт работать. Клеймо ставят на коробку, а объектный тип ничего на объекте не оставляет. Объект, собранный литералом { phone: "555-01" }, при запуске не помнит, что в коде его называли Courier.

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

Объектный тип проверили через instanceof:

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

function contact(d: Courier | Pickup) {
    if (d instanceof Courier) { // ✗
        return "call " + d.phone;
    }
    return "pick up at " + d.address;
}
TEXT
error TS2693: 'Courier' only refers to a type, but is being used as a value here.

Courier — тип, а типы при компиляции стираются. В JavaScript-файле не осталось ничего, с чем instanceof мог бы сравнить объект при запуске, поэтому tsc не пропускает проверку. Тип не сужается, и ошибки появятся ещё на d.phone и d.address. Для объектных типов из type пишите "phone" in d.

Главное

  • Для обычного объекта и массива typeof возвращает "object", поэтому объекты различают другими проверками.
  • "phone" in d оставляет в ветке варианты, у которых объявлено свойство phone, а после неё — остальные.
  • x instanceof Date сужает тип до Date, а Array.isArray(x) — до массива.
  • instanceof работает только с классами: тип из type tsc в нём не пропустит.