Type guards

Prove at runtime what the compiler is being asked to believe.

10of 14
≈ 12Min read
8Parts

The built-in guards

ordinary JavaScript the compiler understands
typeof value === "string"        // string
typeof value === "number"        // number
typeof value === "function"      // a function
value instanceof Date            // a class
Array.isArray(value)             // an array member of a union
"days" in value                  // the members that have that key
value === null                   // null out of a union
value?.name                      // narrows as it goes
typeof null is "object"

So typeof value === "object" includes null. Every object check needs value !== null beside it — which is why the pattern is value && typeof value === "object".

Truthiness narrows, and lies about numbers

the empty string and zero are the trap
function label(count?: number) {
  if (!count) return "None";        // 0 lands here too, and 0 is a real count
  return `${count} doors`;
}

if (count !== undefined) { … }      // say what you mean

A type predicate

value is Door is a promise you make to the compiler. It believes you completely — so a predicate that checks two of five fields is a lie that will be believed for as long as the code lives.

a function whose return value narrows
function isDoor(value: unknown): value is Door {
  return (
    typeof value === "object" && value !== null &&
    "name" in value && typeof (value as { name: unknown }).name === "string" &&
    "days" in value && typeof (value as { days: unknown }).days === "number"
  );
}

const data: unknown = JSON.parse(text);
if (isDoor(data)) {
  data.name;          // Door
}

Predicates in filter

the practical use you will reach for weekly
const maybe: (Door | undefined)[] = …;

const doors = maybe.filter((d): d is Door => d !== undefined);   // Door[]

// and a reusable one
const isPresent = <T>(v: T | null | undefined): v is T => v != null;
const doors = maybe.filter(isPresent);

Assertion functions

narrow for the rest of the scope, or throw
function assertDoor(value: unknown): asserts value is Door {
  if (!isDoor(value)) throw new Error("Not a door");
}

const data: unknown = await res.json();
assertDoor(data);
data.name;          // Door, from here on
It must be a declared function

An assertion function cannot be an arrow assigned to a const without an explicit type annotation — the compiler needs the declaration to trust the assertion.

Discriminants beat predicates

A predicate is for data you did not create. Inside your own program, a discriminated union does the same job with no runtime cost and no chance of the check drifting from the type.

when you own both ends, do not check at all
type Result =
  | { status: "ready"; doors: Door[] }
  | { status: "error"; message: string };

if (result.status === "ready") { … }     // no predicate needed

The boundary belongs to a schema

One declaration produces both the runtime check and the type, so they cannot disagree. This is the answer to the whole "types are erased" problem at the edges of a program.

what to use in real code
import { z } from "zod";

const Door = z.object({
  name: z.string().min(2),
  days: z.number().int().positive(),
  price: z.number().positive(),
});

type Door = z.infer<typeof Door>;        // the type comes from the schema

const parsed = Door.safeParse(await res.json());
if (!parsed.success) return { error: parsed.error.issues };
parsed.data.name;                         // checked at runtime AND typed

Catching errors

a caught value is unknown under strict
try {
  await save();
} catch (err) {
  if (err instanceof Error) console.error(err.message);
  else console.error("Unknown failure", err);
}

// anything can be thrown: throw "a string" is legal JavaScript

Practice