Capítulo 3 de 36

Chapter 3: Narrowing

Core Idea

TypeScript overlays type analysis on top of JavaScript's own runtime control-flow constructs (if/else, switch, ternaries, loops, truthiness, assignments) — every check you'd naturally write in JS to distinguish values also narrows the static type, no special syntax required.

Key Concepts

  • typeof type guards: typeof x === "number" narrows x inside that branch. Values: "string" | "number" | "bigint" | "boolean" | "symbol" | "undefined" | "object" | "function". Gotcha: typeof null === "object", so a typeof x === "object" check does not exclude null.
  • Truthiness narrowing: if (x) coerces via JS truthiness rules (falsy: 0, NaN, "", 0n, null, undefined). Convenient for filtering out null/undefined, but risky on primitives — if (strs) also filters out a legitimate empty string, which is easy to do by accident.
  • Equality narrowing (===, !==, ==, !=, switch): comparing two union-typed values narrows both to their common type. Comparing against == null narrows out both null and undefined at once (JS's loose-equality quirk, and TS understands it).
  • in operator narrowing: "prop" in x narrows a union to the member(s) that have that property (optional properties count as present on both branches).
  • instanceof narrowing: x instanceof Date narrows based on the prototype chain — the standard guard for class instances.
  • Assignment narrowing: assigning a new value to a variable narrows its observed type to what was assigned, but only within the bounds of its declared type (string | number still lets you assign either a string or a number back after narrowing to one).
  • Control flow analysis: TypeScript tracks reachability, not just lexical branches — an early return/throw removes that case from the type for the rest of the function, and type can differ at every point along diverging/re-merging branches.
  • User-defined type guards (type predicates): a function returning param is Type (e.g. function isFish(pet: Fish | Bird): pet is Fish) — calling it narrows the argument in the if/else branches, and works as an Array.prototype.filter predicate to narrow an array's element type.
  • Assertion functions: complementary mechanism (asserts x is T) for narrowing via a function that throws instead of returning a boolean — mentioned only in passing in the handbook page itself, full treatment is elsewhere.
  • Discriminated unions: give every union member a shared literal-typed field (a discriminant, e.g. kind: "circle" vs kind: "square"), each with its own required (not optional) properties. Checking the discriminant (if/switch on shape.kind) then narrows to the exact member — far more reliable than optional properties + non-null assertions on one flat interface.
  • never and exhaustiveness checking: narrowing every member out of a union leaves never (a type nothing can occupy, but which is itself assignable to anything). Assigning the leftover value to a never-typed variable in a switch's default case makes the compiler flag it if a new union member is ever added and left unhandled — turns "did I handle every case?" into a compile error instead of a runtime surprise.

Code Examples

interface Circle { kind: "circle"; radius: number }
interface Square { kind: "square"; sideLength: number }
type Shape = Circle | Square;

function getArea(shape: Shape) {
  switch (shape.kind) {
    case "circle": return Math.PI * shape.radius ** 2; // narrowed to Circle
    case "square": return shape.sideLength ** 2;        // narrowed to Square
    default:
      const _exhaustiveCheck: never = shape; // compile error if a member is added and unhandled
      return _exhaustiveCheck;
  }
}
  • What it demonstrates: the discriminated-union + exhaustiveness-check pattern — the idiomatic way to model a closed set of variant shapes safely.

Reference Tables

Narrowing techniqueTriggerNarrows on
typeoftypeof x === "string"primitive kind (note: typeof null === "object")
Truthinessif (x) / !xfilters falsy values (0, "", NaN, 0n, null, undefined)
Equality===/==/switchliteral values or common type between two unions; == null catches both null and undefined
in"prop" in xunion members that declare prop
instanceofx instanceof Classprototype chain membership
Type predicatefunction f(x): x is Twhatever the predicate asserts, incl. through .filter()
Discriminant checkx.kind === "circle"union member sharing that literal-typed field

Key Takeaways

  1. Model variant data as a discriminated union (shared literal kind/type field, required — not optional — per-member properties) instead of one interface with a pile of optional fields; it eliminates most non-null assertions.
  2. Use the never-assignment trick in a switch default to get a compile-time guarantee that every union member is handled — critical when a union is likely to grow.
  3. Be careful with blanket truthiness checks (if (strs)) on values that could legitimately be "" or 0 — they silently swallow the valid-but-falsy case.
  4. == null / != null is a deliberate, TypeScript-aware idiom for "not null and not undefined" — prefer it over two separate strict checks when you want to exclude both.

Connects To

  • Everyday Types: union types and optional properties, the raw material narrowing operates on.
  • Classes: this is Type narrowing for methods, an extension of the type-predicate mechanism.
  • Object Types: more on discriminated unions and structural typing.