Notifications

No notifications

/Phase 2

Type Narrowing & Guards

Narrowing is how TypeScript turns a wide type (like string

number
null) into a *narrower* one inside a code branch. Master typeof, instanceof, in, equality narrowing, custom type predicates (x is T), and assertion functions and you'll write code that's both safer and clearer.

On this page

Detailed Theory

What is narrowing?

Inside an if / switch / ?: branch, TS uses your runtime checks to *shrink* the type of a variable.

function len(x: string | string[]): number {
  if (typeof x === "string") {
    return x.length;     // x is string here
  }
  return x.length;       // x is string[] here
}

Each kind of check unlocks a different narrowing.

typeof — primitives

function pad(x: string | number) {
  if (typeof x === "string") return x.padStart(4, "0");
  return x.toFixed(2);
}

typeof returns: "string"

"number""boolean""bigint""symbol""undefined""object"
"function". Note: typeof null === "object" (legacy JS bug), so always check null separately.

Truthiness narrowing

function greet(name?: string) {
  if (name) {                  // narrows away undefined AND ""
    console.log(name.toUpperCase());
  }
}

Be careful: if (count) excludes 0 too. Use explicit != null when you only want to filter null/undefined.

if (count != null) { /* count is number, including 0 */ }

Equality narrowing

function example(x: string 
number, y: string
boolean) { if (x === y) { // both are string here — only common type x.toUpperCase(); y.toUpperCase(); } }

instanceof — classes

function format(d: Date | string) {
  if (d instanceof Date) return d.toISOString();
  return d;
}

in — property existence

type Cat = { meow(): void };
type Dog = { bark(): void };

function speak(p: Cat | Dog) { if ("meow" in p) p.meow(); else p.bark(); }

Discriminated union narrowing (recap)

The cleanest form — switch on a literal tag.

type Shape =
  | { kind: "circle"; r: number }
  | { kind: "square"; s: number };

function area(s: Shape) { switch (s.kind) { case "circle": return Math.PI * s.r ** 2; case "square": return s.s ** 2; } }

Custom type predicates — x is T

When the built-in checks aren't enough, write a function whose return type is a *type predicate*. Truthy return means TS narrows the argument to T.

interface Fish { swim(): void }
interface Bird { fly(): void }

function isFish(p: Fish | Bird): p is Fish { return (p as Fish).swim !== undefined; }

function move(p: Fish | Bird) { if (isFish(p)) p.swim(); else p.fly(); }

Use predicates for filtering arrays — TS narrows the result automatically:

const things: (Fish | Bird)[] = [/* … */];
const fishOnly = things.filter(isFish);   // Fish[]

> Without the predicate, filter would return (Fish | Bird)[] — TS can't see through your callback's logic.

Strict predicates with array generics (TS 5.5+)

Array methods like filter now infer narrowed types automatically when the callback returns a clearly narrowing value:

const xs = ["a", null, "b"];
const strs = xs.filter((x) => x !== null);   // string[]

Assertion functions — asserts x is T

Like a predicate, but for *throw-or-pass* style validators.

function assertString(x: unknown): asserts x is string {
  if (typeof x !== "string") throw new Error("Expected string");
}

function shout(x: unknown) { assertString(x); return x.toUpperCase(); // x is string from this line on }

Node's built-in assert and Zod's .parse use this pattern.

Control-flow analysis & reassignment

TS tracks flow even across reassignment:

let x: string | number = 0;
x = "abc";       // narrowed to string
x.toUpperCase(); // ✅

But it widens back if you reassign with the original type:

function pick(): string | number { return 1; }

let v = pick(); if (typeof v === "string") { v.toUpperCase(); // ✅ v = pick(); // back to string | number // v.toUpperCase(); // ❌ }

! — non-null assertion

A short way of telling TS "trust me, this isn't null/undefined". Use sparingly.

const root = document.getElementById("app")!;  // HTMLElement, not HTMLElement 
null

If you're wrong, you'll get Cannot read properties of null at runtime — TS won't save you.

Cheat sheet

CheckNarrows on
typeof x === "string"primitives
x instanceof Fooclasses
"prop" in xobject structure
x === literalliteral types
Array.isArray(x)arrays
x !== nullstrips null
isFoo(x) predicateanything
assertFoo(x)anything (or throws)