---
title: "TypeScript Narrowing Patterns Cheatsheet"
slug: "typescript-narrowing-patterns"
category: "cheatsheets"
tags: ["cheatsheets", "typescript", "narrowing", "type-guards", "reference", "frontend", "backend"]
status: "stable"
last_updated: 2026-05-14
summary: "TypeScript narrowing reference: typeof, instanceof, in, type predicates, discriminated unions, assertion functions, and exhaustive switch patterns."
related:
  [
    "[[coding/typescript-narrowing]]",
    "[[coding/typescript]]",
    "[[coding/typescript-types-vs-interfaces]]",
    "[[coding/typescript-strict-mode]]",
    "[[cheatsheets/typescript-utility-types]]",
    "[[coding/typescript-runtime-validation]]",
    "[[cheatsheets/index|Cheatsheets]]",
  ]
---

## Overview

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 [[coding/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`. |

```typescript
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. |
| Array check | `Array.isArray(x)` | `x: T[]` within the branch. |

```typescript
function handle(err: Error | Response) {
  if (err instanceof Response) {
    console.log(err.status);  // err: Response
  } else {
    console.log(err.message); // err: Error
  }
}
```

## Type predicates

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. |

```typescript
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. |

```typescript
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. |

```typescript
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");
}

// Usage
declare 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.

## Related

- [[coding/typescript-narrowing]]
- [[coding/typescript]]
- [[coding/typescript-types-vs-interfaces]]
- [[coding/typescript-strict-mode]]
- [[cheatsheets/typescript-utility-types]]
- [[coding/typescript-runtime-validation]]
- [[cheatsheets/index|Cheatsheets]]
