The built-in guards
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
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
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.
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
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
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
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.
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.
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
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