---
title: "08 — Narrowing: Refining Types as Code Runs"
uid: narrowing
tags: ["typescript", "narrowing", "roadmap:typescript", "typeof", "instanceof", "type-guards"]
excerpt: "Narrowing is the compiler refining a broad type to a specific one based on checks in the code; guards (typeof, instanceof, equality, predicates) are the checks that trigger it."
date: 2026-08-13T03:27:28+0000
source: https://www.aveshina.my.id/en/blog/narrowing
---

"The compiler just figures it out" was my narrowing model, which made unions feel like a fight. Writing it down made the mechanism explicit: **narrowing is the compiler refining a broad type to a more specific one based on checks in the code, and type guards are the specific checks (typeof, instanceof, equality, truthiness, predicates) that trigger the refinement.** [1] Once I could name the triggers, union types stopped being awkward to work with.

The problem narrowing solves is direct. A variable typed string | number could be either. Inside a function I want to call .toUpperCase() if it's a string and .toFixed() if it's a number — but the compiler only allows operations common to *all* members of the union. Without narrowing, neither method is safe. Narrowing is how I prove to the compiler, at a specific point in the code, that the value is one specific member, unlocking that member's operations.

```figure
<svg viewBox="0 0 720 260" xmlns="http://www.w3.org/2000/svg" class="my-6 w-full max-w-2xl" role="img" aria-label="Narrowing flow. A top bar labelled value: string | number splits at a check 'typeof value'. The left branch is labelled string, the right branch labelled number, each with its own safe method available.">
  <defs>
    <marker id="narrowarrow" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto">
      <path d="M0,0 L10,5 L0,10 z" fill="#64748b"/>
    </marker>
  </defs>
  <g font-family="ui-sans-serif, system-ui, sans-serif" text-rendering="geometricPrecision">

    <!-- broad type -->
    <rect x="240" y="20" width="240" height="40" rx="8" fill="#e0e7ff" stroke="#6366f1" stroke-width="1.5"/>
    <text x="360" y="45" font-size="13" font-weight="700" fill="#1e1b4b" text-anchor="middle">value: string | number</text>

    <!-- check -->
    <rect x="270" y="90" width="180" height="36" rx="8" fill="#fef9c3" stroke="#ca8a04" stroke-width="1.5"/>
    <text x="360" y="113" font-size="11" font-family="ui-monospace, monospace" fill="#422006" text-anchor="middle">if (typeof value)</text>

    <!-- left branch: string -->
    <path d="M300,126 L160,170" fill="none" stroke="#64748b" stroke-width="1.5" marker-end="url(#narrowarrow)"/>
    <rect x="60" y="172" width="200" height="64" rx="8" fill="#dcfce7" stroke="#16a34a" stroke-width="1.5"/>
    <text x="160" y="195" font-size="12" font-weight="700" fill="#052e16" text-anchor="middle">narrowed to string</text>
    <text x="160" y="218" font-size="11" font-family="ui-monospace, monospace" fill="#052e16" text-anchor="middle">value.toUpperCase()</text>

    <!-- right branch: number -->
    <path d="M420,126 L560,170" fill="none" stroke="#64748b" stroke-width="1.5" marker-end="url(#narrowarrow)"/>
    <rect x="460" y="172" width="200" height="64" rx="8" fill="#fce7f3" stroke="#db2777" stroke-width="1.5"/>
    <text x="560" y="195" font-size="12" font-weight="700" fill="#500724" text-anchor="middle">narrowed to number</text>
    <text x="560" y="218" font-size="11" font-family="ui-monospace, monospace" fill="#500724" text-anchor="middle">value.toFixed(2)</text>
  </g>
</svg>
```

## typeof guards

The **typeof** check narrows based on JavaScript's runtime typeof operator [1]. For the primitives it knows about — string, number, boolean, symbol, undefined, "function" — a typeof comparison tells the compiler which branch holds which type:

```
function format(value: string | number) {
  if (typeof value === "string") {
    return value.toUpperCase(); // safe: narrowed to string
  }
  return value.toFixed(2);      // safe: narrowed to number
}
```

Inside the if block, the compiler treats value as string; after the if, it knows the string case is exhausted, so value is number. typeof is the go-to for primitive unions because it reads at runtime exactly the way the compiler interprets it.

## instanceof guards

The **instanceof** operator narrows based on an object's prototype chain — whether it's an instance of a specific class or constructor [2]:

```
if (error instanceof TypeError) {
  error.message; // safe: narrowed to TypeError
}
```

This is the right tool when a union contains different class types (e.g., Error | TypeError | SyntaxError). The check works at runtime by walking the prototype chain, and the compiler mirrors that to narrow the type. It only works on values that have a constructor, so it's for class-based unions, not primitives.

## Equality narrowing

**Equality checks** (===, !==, ==, !=) narrow by comparing a value against a specific literal [3]:

```
function respond(method: "GET" | "POST") {
  if (method === "GET") {
    // method narrowed to "GET"
  }
}
```

This is especially powerful with literal unions and discriminated unions — comparing against a literal value (or another variable of a known type) lets the compiler rule out that case and narrow to the rest. Equality narrowing is what makes discriminated unions (where each member has a shared literal kind field) feel like a typed switch.

## Truthiness narrowing

**Truthiness** checks narrow based on whether a value is truthy or falsy [4]. null, undefined, 0, "", NaN, and false are falsy; everything else is truthy. A simple if (value) filters out null and undefined (and the other falsy values) from the type inside the block:

```
function greet(name: string | null) {
  if (name) {
    name.toUpperCase(); // safe: null filtered out
  }
}
```

Truthiness is the everyday tool for optional values — the idiomatic if (maybeValue) before using it. The catch is that it also filters 0 and "", which is usually fine but occasionally a bug when those are legitimate values; a strict === null check is the precise alternative when I only want to exclude absence.

## Type predicates: custom guards

When the built-in checks aren't enough, a **type predicate** lets me write a function that narrows for the compiler [1]. The syntax is x is Type as a return type:

```
function isString(value: unknown): value is string {
  return typeof value === "string";
}

if (isString(maybe)) {
  maybe.toUpperCase(); // safe: predicate narrowed it
}
```

The function returns a boolean at runtime, but the value is string return type tells the compiler: when this returns true, treat the argument as string. Type predicates are how I encapsulate complex narrowing logic — checking a discriminated union's shape, validating parsed data, or building reusable guards — and reuse it across a codebase. They're the bridge between runtime validation and compile-time types.

## How I use this

My narrowing ladder, from most to least common: if (value) truthiness for optional values; typeof for primitive unions; equality checks for literal and discriminated unions; instanceof for class unions; and a **type predicate** when I need to reuse a check or express something the built-ins can't. The mental shift is that narrowing isn't magic — it's the compiler tracking control flow through specific, nameable checks. When a narrowing doesn't work as expected, I ask which trigger I'm missing, and the answer is usually "extract a type predicate."

## References

[1] Microsoft, "typeof type guards" and "Using type predicates," TypeScript Handbook, 2024. [Online]. Available: [https://www.typescriptlang.org/docs/handbook/2/narrowing.html#typeof-type-guards](https://www.typescriptlang.org/docs/handbook/2/narrowing.html#typeof-type-guards)

[2] Microsoft, "instanceof narrowing," TypeScript Handbook, 2024. [Online]. Available: [https://www.typescriptlang.org/docs/handbook/2/narrowing.html#instanceof-narrowing](https://www.typescriptlang.org/docs/handbook/2/narrowing.html#instanceof-narrowing)

[3] Microsoft, "Equality narrowing," TypeScript Handbook, 2024. [Online]. Available: [https://www.typescriptlang.org/docs/handbook/2/narrowing.html#equality-narrowing](https://www.typescriptlang.org/docs/handbook/2/narrowing.html#equality-narrowing)

[4] Microsoft, "Truthiness narrowing," TypeScript Handbook, 2024. [Online]. Available: [https://www.typescriptlang.org/docs/handbook/2/narrowing.html#truthiness-narrowing](https://www.typescriptlang.org/docs/handbook/2/narrowing.html#truthiness-narrowing)

```quiz
Q: A variable is `string | number`. Inside `if (typeof value === "string")`, what is the type of value?
- string
- string | number
correct: 0
explain: typeof narrowing refines the type inside the block. The compiler tracks that the condition holds, so value is treated as string there and number after.

Q: Which narrowing tool is best for a union of different class types like Error | TypeError?
- typeof
- instanceof
correct: 1
explain: instanceof walks the prototype chain and narrows class-based unions. typeof only distinguishes primitive kinds, not specific classes.

Q: Truthiness check `if (name)` on `string | null` filters out null. What else does it also filter out?
- nothing else
- the other falsy values: "", 0, false, undefined, NaN
correct: 1
explain: Truthiness treats all falsy values the same. This is usually fine for optional values but can be a bug when "" or 0 are legitimate — use === null for precision.

Q: A function declared with return type `value is string` is called a…
- type predicate
- type assertion
correct: 0
explain: A type predicate (x is Type) tells the compiler that when the function returns true, the argument should be narrowed. It's how custom, reusable narrowing is expressed.

Q: Equality narrowing shines especially with…
- any union at all
- literal unions and discriminated unions, where comparing against a specific value rules out that case
correct: 1
explain: Comparing a value to a literal (=== "GET") lets the compiler remove that case and narrow to the remaining members. This is what makes discriminated unions feel like a typed switch.
```
