Skip to content
Theory · Narrowing · lesson 5 of 12

The in and instanceof checks

Boxes have arrived on the sorting belt. From the outside they look the same, all cardboard, and the typeof arch says one thing about each of them: "a box". To sort the boxes further, you need other marks. For example, whether a box has a carrying handle, or whether it bears the stamp of the factory that made it.

Objects in JavaScript are the same: for a plain object, an array or a date, typeof returns "object". A delivery service keeps two kinds of orders: courier delivery, which has a phone number, and pickup, which has the address of a pickup point. A typeof check cannot tell them apart:

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'.

The condition typeof d === "object" is true for both kinds of order, so tsc narrows nothing: the branch still has Courier | Pickup. We need a check that looks at the object itself.

The in operator

JavaScript's in operator checks at run time whether an object has a property with that name. tsc reads the same check and keeps in the branch only the members of the union that declare this property:

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

Let's walk through the example line by line:

  1. When the program runs, "phone" in d asks the object whether it has the key phone.
  2. Inside the if, d has the type Courier: only it declares the phone property.
  3. After the return, Pickup is left, and d.address compiles: narrowing works both ways.
  4. The program prints call 555-01 and pick up at Main St 5.

This is the handle on the box: the check looks for a part the object has. in needs no classes: it works with plain object types described with type.

instanceof and Array.isArray

The other mark is the factory stamp. Objects created with new are checked with the instanceof operator. It tells which constructor made the object: Date, Error, or a JavaScript class you wrote yourself. An array is checked with the Array.isArray function:

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

The program prints 2024 2. Let's go through each check.

instanceof

A date comes as a Date object or as a string like "2025-01-31". Inside if (d instanceof Date), d has the type Date, and the getFullYear method is available. After the return, a string is left, and d.slice compiles. The call year(new Date(2024, 4, 1)) returns 2024.

The same check works on an error e caught in a catch: after e instanceof Error, e has the type Error inside the branch, and you can read e.message.

Array.isArray

Tags come as a single string or as an array of strings. Array.isArray(tags) returns true only for an array, and tsc keeps the type string[] in the branch. The call count(["a", "b"]) returns 2, and a single string counts as one tag.

For an array typeof also gives "object", so this is a job for Array.isArray.

in or instanceof

Interviewers like to ask this. In short, the difference is:

  • in checks whether an object has a property with that name, and it suits any object, including ones described with type;
  • instanceof checks which constructor created an object, and it works only with classes.

Here the stamp picture parts ways with TypeScript. A stamp is put on the box, while an object type leaves nothing on the object. An object built with the literal { phone: "555-01" } does not remember at run time that the code called it a Courier.

Common mistake

An object type is checked with 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 is a type, and types are erased at compile time. Nothing is left in the JavaScript file that instanceof could compare the object with at run time, so tsc rejects the check. The type is not narrowed, and more errors appear on d.phone and d.address. For object types made with type, write "phone" in d.

Key points

  • For a plain object or an array, typeof returns "object", so objects are told apart with other checks.
  • "phone" in d keeps in the branch the members that declare a phone property, and the rest after it.
  • x instanceof Date narrows the type to Date, and Array.isArray(x) to an array.
  • instanceof works only with classes: tsc does not allow a type in it.