Warm-up · Activity 1 of 7
// 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.
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));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): {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; }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;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)); }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.tsOutput
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
Hint 1
unknown allows nothing until you narrow it. Which check tells you that a value is an Error?
Hint 2
error instanceof Error is true for a TypeError too, because TypeError extends Error.
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.tsRun the checks (needs learnrun.js in the same folder):
npx tsc
node --testDownload learnrun.jsExercise 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
Hint 1
The body of isBook is fine. The problem is what its return type tells tsc.
Hint 2
A type predicate has the form parameterName is Type, with the parameter of this function.
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.tsRun the checks (needs learnrun.js in the same folder):
npx tsc
node --testDownload learnrun.jsCommon 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.