Type Predicates & Type Guards at Scale
🛡️ Type Predicates & Type Guards at Scale
isDog(pet): pet is Dog works fine for one union in one file. This chapter is about what happens when a codebase has dozens of unions to narrow — reusable guard factories, guards that combine with and/or, and assertion functions, so narrowing logic stops being copy-pasted everywhere it's needed.
Recap: The Three Kinds of Guard
typeof / instanceof
Built into the language, work automatically — no is syntax needed for primitives and class instances.
Custom value is Type
A function you write for shapes the built-ins can't check — the pattern Course 2 introduced for Dog vs Cat.
The gap this chapter fills: writing one narrow guard per union, per file, does not scale to a codebase with fifty discriminated unions.
🏭 Reusable Guard Factories
Instead of a bespoke guard for every object shape, a generic factory can check "does this object have property K" once, and be reused everywhere:
This one function replaces a whole family of one-off "does this have an email field" / "does this have a total field" checks that would otherwise be duplicated per union.
Combining Guards With and / or
Real checks are often several conditions at once — combinators let you build a compound guard out of smaller, reusable ones instead of writing a new function every time:
Generic Guard Combinators
and
Both guards must pass — narrows to the more specific of the two, useful for "is a string AND non-empty" style checks.
or
Either guard passing is enough — narrows to the union B | C, useful for "is a Dog OR a Cat" style checks without a discriminant.
A Guard Factory for Discriminated Unions
Chapter 3's discriminated unions (ApiResponse<T>, ClientMessage) all follow the same shape — a factory can generate their guards instead of hand-writing a switch for each one:
Extract<T, U> is a built-in conditional type (implemented with the same distributive trick from Chapter 1) — isVariant reuses it instead of writing a bespoke narrowing type per union.
✅ Assertion Functions
A type guard returns a boolean you branch on. An assertion function throws if the check fails, narrowing everything after the call instead of inside an if block:
asserts value is T
Narrows value to T for every line after the call — no if wrapper required, at the cost of throwing instead of returning false.
asserts condition
A simpler form with no specific type — just tells the compiler "if we got past this line, some earlier possibility is now ruled out" (e.g. narrowing away undefined).
💻 Coding Challenges
Challenge 1: Build a Generic hasProperties
Generalize the chapter's hasProperty to hasProperties<K extends PropertyKey>(obj: unknown, ...keys: K[]), checking that all given keys exist on the object.
Goal: Practice extending a single-key guard to a variadic, multi-key one using the tuple/rest patterns from Chapter 3.
Challenge 2: Write an isOneOf Guard
Write a generic isOneOf<T extends readonly unknown[]>(value: unknown, options: T): value is T[number] that checks whether value matches any element of a fixed array of allowed values.
Goal: Practice a reusable guard for "is this one of these specific literal values" — the enum-like check pattern used constantly for validating string literals.
Challenge 3: Write an Assertion Function
Write assertIsRecord(value: unknown): asserts value is Record<string, unknown>, then use it at the top of a function to avoid repeating null/type checks on every subsequent property access.
Goal: Practice the asserts value is T pattern as an alternative to an if-wrapped type guard.
function isString(v: unknown): v is string { return true; } compiles perfectly and will happily narrow anything to string — TypeScript trusts a type predicate's return type completely and performs zero runtime verification of its own. A type guard is only as safe as the check actually inside its body; a guard reused across dozens of call sites that's subtly wrong (or, worse, a stub left over from early development) is a much bigger blast radius than a single hand-written check that was wrong in one place.
🎯 What's Next
Guards prove what a value is at runtime — the next chapter covers proving what a value means, even when two values share the exact same runtime representation: Branded Types & Phantom Types, encoding domain constraints (like "this string was validated" or "this number is a UserId, not an OrderId") directly into the type system.