TypeScript’s control-flow analysis understands certain patterns and narrows the type of a variable to a more specific subtype within a branch. This card catalogs every narrowing mechanism, when to use each, and the exhaustive-switch pattern for discriminated unions. The rationale for narrowing over casting lives in typescript-narrowing.
Primitive narrowing guards
Use these for standard JS value checks.
Guard
Example
Narrows to
typeof x === "string"
if (typeof x === "string")
string
typeof x === "number"
if (typeof x === "number")
number
typeof x === "boolean"
if (typeof x === "boolean")
boolean
typeof x === "undefined"
if (typeof x === "undefined")
undefined
typeof x === "function"
if (typeof x === "function")
Function
x === null
if (x === null)
null
x != null
if (x != null)
Removes null and undefined from the type.
Truthiness
if (x)
Removes null, undefined, 0, "", false.
function printLength(value: string | number | null) { if (value === null) return; // null removed if (typeof value === "string") { console.log(value.length); // value: string } else { console.log(value.toFixed(2)); // value: number }}
Object and class narrowing
Guard
Example
Narrows to
instanceof C
if (err instanceof TypeError)
TypeError (subtype of Error).
"prop" in obj
if ("name" in obj)
Type that includes the name property.
Optional chaining
obj?.prop
Does not narrow; use in or type predicate instead.
Write a function that tells TypeScript the exact type when it returns true.
Pattern
Signature form
When to use
Single-type predicate
function isX(v: unknown): v is X
Narrow from unknown or a union to one concrete type.
Falsy predicate
function isDefined<T>(v: T | null | undefined): v is T
Reusable null/undefined guard.
Branded type guard
Predicate that checks a _brand property.
Distinguish opaque types that have the same structural shape.
interface Cat { meow(): void; }interface Dog { bark(): void; }function isCat(animal: Cat | Dog): animal is Cat { return "meow" in animal;}function makeNoise(animal: Cat | Dog) { if (isCat(animal)) { animal.meow(); // animal: Cat } else { animal.bark(); // animal: Dog }}
Discriminated unions
Add a shared literal field (kind, type, tag) so TypeScript can narrow on its value.
Requirement
Example
Effect
Common discriminant field
type: "circle" | "square"
Must be a literal type (string, number, boolean).
Exhaustive switch
switch (shape.type) with default: assertNever(shape)
Compile error when a new variant is added but not handled.
Inline narrowing
if (shape.type === "circle")
Works without a switch; useful for early returns.
type Shape = | { type: "circle"; radius: number } | { type: "rect"; width: number; height: number } | { type: "triangle"; base: number; height: number };function area(s: Shape): number { switch (s.type) { case "circle": return Math.PI * s.radius ** 2; case "rect": return s.width * s.height; case "triangle": return 0.5 * s.base * s.height; default: return assertNever(s); }}
Assertion functions and assertNever
Pattern
Signature
Effect
assertNever
function assertNever(x: never): never { throw new Error(...) }
Causes a compile error if x is not never; exhaustiveness check.
Assertion function
function assert(cond: boolean, msg: string): asserts cond
TypeScript treats code after the call as having cond be true.
Type-narrowing assertion
function assertIsString(v: unknown): asserts v is string
Narrows v to string for all code following the call.
function assertNever(x: never): never { throw new Error(`Unhandled case: ${JSON.stringify(x)}`);}function assertDefined<T>(v: T | null | undefined): asserts v is T { if (v == null) throw new Error("Expected defined value");}// Usagedeclare const user: { name: string } | null;assertDefined(user);console.log(user.name); // user: { name: string } — TS knows it's defined
Common gotchas
typeof null === "object" is a JavaScript legacy quirk. Always check x !== null before trusting an "object" typeof result.
Type predicates v is T are not verified by the compiler. A predicate that returns true for the wrong type silently lies to TypeScript. Keep predicates simple and obvious.
in narrowing requires the operand to be an object or union of objects. Using "prop" in x where x could be null or undefined throws at runtime. Guard with x != null first.
Discriminated unions only work when the discriminant is a literal type, not string or number. type: string provides no narrowing.
Assertion functions (asserts cond) are not checked by the compiler for correctness. They narrow downstream types unconditionally, so a buggy assertion can mask type errors.
The default: assertNever(s) pattern only catches unhandled variants at compile time. At runtime, the throw is your guard against future discriminant values arriving from external data.