Skip to content
aviral gupta

// B3.3 · ~30 min · Beginner

instanceof and type predicates

After this lesson you can narrow with instanceof, use a caught error safely, predict a variable's type after assignments and returns, and write a type predicate that also filters arrays.

Lesson 3 of 5 in B3 Narrowing

You will be able to

  • Narrow with instanceof, including the unknown value in a catch clause
  • Predict the type of a variable after assignments, returns and throws (control flow analysis)
  • Write type predicates such as pet is Fish, and filter arrays with them
  1. Warm-up · Activity 1 of 7

    Warm-up from JavaScript: which of these expressions are true? Pick all that apply.

    Select all that apply.

  2. Predict · Activity 2 of 7

    Predict before you read on: values has the type (number | null)[]. What happens when you run npx tsc and node main.ts?

    const values = [3, null, 7, null];
    const numbers = values.filter((v) => v !== null);
    console.log(numbers.map((n) => n * 2));
  3. Practice · Activity 3 of 7

    Fill in the return type of isFish, so that tsc accepts move and the program prints swims and flies.

    type Fish = { name: string; swim: () => string };
    type Bird = { name: string; fly: () => string };
    type Pet = Fish | Bird;
    
    function isFish(pet: Pet): ____ {
      return "swim" in pet;
    }
    
    function move(pet: Pet): string {
      return isFish(pet) ? pet.swim() : pet.fly();
    }
    
    console.log(move({ name: "Nemo", swim: () => "swims" }));
    console.log(move({ name: "Tweety", fly: () => "flies" }));
    function isFish(pet: Pet): {
  4. Practice · Activity 4 of 7

    With the types below, match each situation to the type the variable has at that point.

    type Fish = { swim: () => void };
    type Bird = { fly: () => void };
    function isFish(pet: Fish | Bird): pet is Fish {
      return "swim" in pet;
    }
  5. Practice · Activity 5 of 7

    Which lines does tsc reject?

    type Id = string | number;
    let id: Id = "a7";
    id = 7;
    console.log(id.toFixed(1));
    id = "b9";
    console.log(id.toUpperCase());
    id = true;
  6. Brain teaser · Activity 6 of 7

    Brain teaser. tsc reports no errors for this file. What happens when it runs?

    function isNumber(x: unknown): x is number {
      return typeof x === "string";
    }
    
    const input: unknown = "42";
    if (isNumber(input)) {
      console.log(input.toFixed(1));
    }
  7. Apply · Activity 7 of 7

    Mini-task. A blog has drafts ({ title }) and published posts ({ title, url }). Write a type predicate isPublished, and links(posts) that uses filter(isPublished) to return "title: url" for each published post. Then write countPosts(text) that parses JSON text and returns the length of the array, or 0; in its catch, use instanceof SyntaxError before you read the error. Check it in the editor, or with npx tsc and node main.ts.

    Check your work against this list

Build it yourself

Read the worked example, then write the exercises. Your code runs in your browser or on your computer and is never uploaded.

Worked example

An event log

A log holds dates, errors and two kinds of plain objects. instanceof picks the Date and the Error (a RangeError is an Error too). Plain objects have no class, so isClick is a type predicate; after it, only KeyPress is left. The same predicate gives log.filter(isClick) the type Click[]. Run npx tsc, then node main.ts. Then change the return type of isClick to boolean and run tsc again.

main.ts

// An event log: instanceof for built-in classes, a type predicate for plain objects.
type Click = { x: number; y: number };
type KeyPress = { key: string };
type LogEntry = Date | Error | Click | KeyPress;

function isClick(entry: LogEntry): entry is Click {
  return "x" in entry && "y" in entry;
}

function describe(entry: LogEntry): string {
  if (entry instanceof Date) return "time " + entry.toISOString().slice(11, 19);
  if (entry instanceof Error) return "error " + entry.message;
  if (isClick(entry)) return "click at " + entry.x + "," + entry.y;
  return "key " + entry.key; // only KeyPress is left
}

const log: LogEntry[] = [
  new Date(Date.UTC(2026, 9, 4, 9, 30, 0)),
  { x: 10, y: 20 },
  { key: "Enter" },
  new RangeError("volume above 100"),
  { x: 4, y: 2 },
];

for (const entry of log) {
  console.log(describe(entry));
}

const clicks = log.filter(isClick); // Click[]: the predicate narrows the array too
console.log(clicks.length + " clicks, x values: " + clicks.map((c) => c.x).join(", "));

Run it with

npx tsc
node main.ts

Output

time 09:30:00
click at 10,20
key Enter
error volume above 100
click at 4,2
2 clicks, x values: 10, 4
  • With boolean instead of entry is Click, entry.x and entry.key are rejected, and so is c.x on the last line.
  • instanceof Error also matches the RangeError, because RangeError extends Error.
  • The Date is printed with toISOString, in UTC, so the output is the same on every computer.
  • filter(isClick) keeps the elements for which isClick returns true, and the predicate tells tsc what they are.
Change it and run it

Tab indents and Shift+Tab outdents. To leave the editor with the keyboard, press Esc, then Tab.

The first run downloads the TypeScript compiler (up to 2.1 MB) and keeps it cached. Your code stays on your device.

Exercises

Exercise 1 of 2

Describe whatever was thrown

errorText gets the value a catch clause received, so its type is unknown. tsc reports error TS18046: 'error' is of type 'unknown'. Return the message for an Error (of any kind), the text itself for a thrown string, and "unknown error" for anything else.

Tab indents and Shift+Tab outdents. To leave the editor with the keyboard, press Esc, then Tab.

The first run downloads the TypeScript compiler (up to 2.1 MB) and keeps it cached. Your code stays on your device.

Hints
  1. Hint 1

    unknown allows nothing until you narrow it. Which check tells you that a value is an Error?

  2. Hint 2

    error instanceof Error is true for a TypeError too, because TypeError extends Error.

  3. Hint 3

    After the instanceof check, add typeof error === "string" for thrown text, and return "unknown error" at the end.

Show a solution

One way to solve it. Yours can look different and still pass the checks.

export function errorText(error: unknown): string {
  if (error instanceof Error) {
    return error.message;
  }
  if (typeof error === "string") {
    return error;
  }
  return "unknown error";
}
Run it on your computer

Install TypeScript 7.0 or newer. Save these files in one folder, open a terminal in that folder, and run the commands below.

main.ts

export function errorText(error: unknown): string {
  return error.message;
}

main.test.ts

import {test} from 'node:test';
import assert from 'node:assert/strict';
import {errorText} from './main.ts';

test('an Error gives its message', () => {
  assert.equal(errorText(new Error('disk full')), 'disk full', 'errorText(new Error("disk full")) should be "disk full"');
});

test('a TypeError gives its message too', () => {
  assert.equal(errorText(new TypeError('bad input')), 'bad input', 'errorText(new TypeError("bad input")) should be "bad input"');
});

test('a thrown string is returned as it is', () => {
  assert.equal(errorText('timeout'), 'timeout', 'errorText("timeout") should be "timeout"');
});

test('anything else gives "unknown error"', () => {
  assert.equal(errorText(42), 'unknown error', 'errorText(42) should be "unknown error"');
  assert.equal(errorText(null), 'unknown error', 'errorText(null) should be "unknown error"');
});

package.json

{
  "type": "module"
}

tsconfig.json

{
  "compilerOptions": {
    "target": "esnext",
    "module": "nodenext",
    "lib": [
      "esnext",
      "dom"
    ],
    "types": [],
    "strict": true,
    "noEmit": true,
    "erasableSyntaxOnly": true,
    "verbatimModuleSyntax": true,
    "allowImportingTsExtensions": true
  },
  "include": [
    "**/*.ts"
  ],
  "exclude": [
    "**/*.test.ts"
  ]
}

npx tsc needs TypeScript in the folder: run npm install --save-dev typescript there once. tsc checks the types; Node.js runs main.ts by removing them.

Run the program:

npx tsc
node main.ts

Run the checks (needs learnrun.js in the same folder):

npx tsc
node --test
Download learnrun.js

Exercise 2 of 2

Count the pages of the books

A shelf holds books and films. totalPages filters the books with isBook, but tsc reports error TS2339: Property 'pages' does not exist on type 'Item', because isBook only says it returns a boolean. Change one thing so that filter(isBook) gives Book[] and the file type-checks.

Tab indents and Shift+Tab outdents. To leave the editor with the keyboard, press Esc, then Tab.

The first run downloads the TypeScript compiler (up to 2.1 MB) and keeps it cached. Your code stays on your device.

Hints
  1. Hint 1

    The body of isBook is fine. The problem is what its return type tells tsc.

  2. Hint 2

    A type predicate has the form parameterName is Type, with the parameter of this function.

  3. Hint 3

    export function isBook(item: Item): item is Book {

Show a solution

One way to solve it. Yours can look different and still pass the checks.

export type Book = { title: string; pages: number };
export type Film = { title: string; minutes: number };
export type Item = Book | Film;

export function isBook(item: Item): item is Book {
  return "pages" in item;
}

export function totalPages(items: Item[]): number {
  let total = 0;
  for (const book of items.filter(isBook)) {
    total += book.pages;
  }
  return total;
}
Run it on your computer

Install TypeScript 7.0 or newer. Save these files in one folder, open a terminal in that folder, and run the commands below.

main.ts

export type Book = { title: string; pages: number };
export type Film = { title: string; minutes: number };
export type Item = Book | Film;

export function isBook(item: Item): boolean {
  return "pages" in item;
}

export function totalPages(items: Item[]): number {
  let total = 0;
  for (const book of items.filter(isBook)) {
    total += book.pages;
  }
  return total;
}

main.test.ts

import {test} from 'node:test';
import assert from 'node:assert/strict';
import {isBook, totalPages} from './main.ts';

const book = {title: 'Dune', pages: 412};
const film = {title: 'Alien', minutes: 117};

test('isBook recognises a book and a film', () => {
  assert.equal(isBook(book), true, 'isBook(book) should be true');
  assert.equal(isBook(film), false, 'isBook(film) should be false');
});

test('totalPages adds up the pages of the books only', () => {
  const shelf = [book, film, {title: 'Emma', pages: 474}];
  assert.equal(totalPages(shelf), 886, 'totalPages should be 412 + 474 = 886');
});

test('totalPages of a shelf without books is 0', () => {
  assert.equal(totalPages([film]), 0, 'totalPages([film]) should be 0');
});

package.json

{
  "type": "module"
}

tsconfig.json

{
  "compilerOptions": {
    "target": "esnext",
    "module": "nodenext",
    "lib": [
      "esnext",
      "dom"
    ],
    "types": [],
    "strict": true,
    "noEmit": true,
    "erasableSyntaxOnly": true,
    "verbatimModuleSyntax": true,
    "allowImportingTsExtensions": true
  },
  "include": [
    "**/*.ts"
  ],
  "exclude": [
    "**/*.test.ts"
  ]
}

npx tsc needs TypeScript in the folder: run npm install --save-dev typescript there once. tsc checks the types; Node.js runs main.ts by removing them.

Run the program:

npx tsc
node main.ts

Run the checks (needs learnrun.js in the same folder):

npx tsc
node --test
Download learnrun.js

Common mistakes

Reading .message from the caught value

try {
  JSON.parse("{oops");
} catch (e) {
  console.log("Could not read the settings: " + e.message);
}

What tsc or Node.js prints

main.ts(4,49): error TS18046: 'e' is of type 'unknown'.

Why, and the fix

Anything can be thrown, not only errors, so with strict the catch variable is unknown. Narrow it first: if (e instanceof Error) { console.log("Could not read the settings: " + e.message); }. Handle other values in an else branch, or turn them into text with String(e).

A type guard that only returns boolean

type Fish = { swim: () => void };
type Bird = { fly: () => void };
type Pet = Fish | Bird;

function isFish(pet: Pet): boolean {
  return "swim" in pet;
}

function move(pet: Pet) {
  if (isFish(pet)) {
    pet.swim();
  }
}

move({ swim: () => console.log("swimming") });

What tsc or Node.js prints

main.ts(11,9): error TS2339: Property 'swim' does not exist on type 'Pet'.

Why, and the fix

The check inside isFish narrows only inside isFish. To the caller, boolean says nothing about pet, so pet is still a Pet. Declare the return type as a type predicate: function isFish(pet: Pet): pet is Fish. Then if (isFish(pet)) narrows pet to Fish, and the else branch to Bird.

instanceof with a primitive type name

function shout(value: unknown): string {
  if (value instanceof string) {
    return value.toUpperCase();
  }
  return "not text";
}

console.log(shout("hi"));

What tsc or Node.js prints

main.ts(2,24): error TS2693: 'string' only refers to a type, but is being used as a value here.

Why, and the fix

The right side of instanceof must be a class that exists at run time. string is only a type; Node.js would stop with ReferenceError: string is not defined. Primitives have no class to test against: use typeof value === "string". instanceof String would compile, but it is false for "hi", which is a primitive and not a String object.

TypeScript in the browser: the TypeScript 6.0.3 compiler, Apache-2.0, then your browser’s own engine. Licence and source

Exit ticket

5 questions, no hints. Score 80% or more to complete the lesson.

Finish every activity above to unlock the exit ticket.

Report a problem

Spotted something wrong or unclear? Say what, and it will be checked and fixed.

#

At least 20 characters.

Only if you want a reply.

Key ideas

instanceof, and the error in catch

x instanceof Foo checks whether Foo.prototype is on the prototype chain of x, and TypeScript narrows on it: with x: Date | string, x is a Date inside if (x instanceof Date) and a string in the else branch. It works for values made with new: Date, Map, Error and its subclasses (a RangeError is also an Error). Its most common use is catch. With strict, the variable in catch (e) has the type unknown, because any value can be thrown; if (e instanceof Error) makes e.message safe. The right side must be a class, a value: x instanceof string is error TS2693, since string is only a type. For primitives, use typeof.

Assignments and control flow analysis

A variable has a declared type, and at each point an observed type. With type Id = string | number and let id: Id = "a7", id is a string until id = 7 makes it a number: then id.toFixed(1) compiles and id.toUpperCase() does not. Assignability is always checked against the declared type, so id = "b9" is fine again, and id = true is error TS2322: Type 'boolean' is not assignable to type 'Id'. tsc follows which code is reachable: after a branch that returns or throws, what it checked is gone for the rest of the function. Where branches meet, the types merge: a string from one branch and a number from the other give string | number.

Type predicates: your own type guards

A function whose return type is a type predicate works like a built-in check: function isFish(pet: Pet): pet is Fish { return "swim" in pet; }. The name before is must be a parameter, or tsc reports error TS1225. if (isFish(pet)) narrows pet to Fish, the else branch to Bird, and zoo.filter(isFish) returns Fish[]. With the return type boolean nothing narrows. tsc does not check that the body tells the truth: return "fly" in pet compiles too. Since TypeScript 5.5, tsc infers a predicate for a function such as v => v !== null with no annotation, but not for v => !!v: false could also mean 0.

Sources

Last reviewed October 4, 2026