Core Concepts7 min read · Updated 2025-06-01

TypeScript Type Guards & Narrowing Explained

Learn every TypeScript narrowing technique: typeof, instanceof, in, discriminated unions, assertion functions, and custom type predicates — with practical examples.

💡 Practice these concepts interactively with AI feedback

Start Practicing →

TypeScript Type Guards & Narrowing Explained

Narrowing is TypeScript's ability to refine a broader type to a more specific one based on runtime checks. It's what makes union types practical.

typeof Narrowing

function format(value: string | number | boolean): string {
  if (typeof value === 'string') {
    return value.toUpperCase()  // value: string ✓
  }
  if (typeof value === 'number') {
    return value.toFixed(2)     // value: number ✓
  }
  return String(value)          // value: boolean ✓
}

typeof works for: 'string', 'number', 'boolean', 'bigint', 'symbol', 'undefined', 'function', 'object'.

instanceof Narrowing

function handleEvent(event: MouseEvent | KeyboardEvent) {
  if (event instanceof MouseEvent) {
    console.log(event.clientX, event.clientY) // MouseEvent properties ✓
  } else {
    console.log(event.key) // KeyboardEvent properties ✓
  }
}

// Works with custom classes too: class ApiError extends Error { constructor(public statusCode: number, message: string) { super(message) } } class NetworkError extends Error { constructor(public retryable: boolean, message: string) { super(message) } }

function handleError(err: ApiError | NetworkError) { if (err instanceof ApiError) { console.log(err.statusCode) // ✓ } else { console.log(err.retryable) // ✓ } }

in Operator Narrowing

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

function speak(animal: Cat | Dog) { if ('meow' in animal) { animal.meow() // animal: Cat ✓ } else { animal.bark() // animal: Dog ✓ } }

Discriminated Unions (Best Practice)

The most robust narrowing pattern — add a literal discriminant field:

type Success<T> = { status: 'success'; data: T }
type Failure = { status: 'failure'; error: string }
type Loading = { status: 'loading' }

type AsyncResult<T> = Success<T> | Failure | Loading

function render<T>(result: AsyncResult<T>) { switch (result.status) { case 'loading': return <Spinner /> // result: Loading ✓ case 'success': return <Data value={result.data} /> // result: Success<T> ✓ case 'failure': return <Error msg={result.error} /> // result: Failure ✓ } }

The discriminant (status) must be a literal type, not just string.

Custom Type Predicates

// Standard approach — TypeScript doesn't learn the type after this:
function isUser(value: unknown) {
  return typeof value === 'object' && value !== null && 'id' in value
}

// Type predicate — TypeScript narrows in the true branch: function isUser(value: unknown): value is User { return ( typeof value === 'object' && value !== null && 'id' in value && typeof (value as any).id === 'number' ) }

const data: unknown = fetchFromAPI() if (isUser(data)) { console.log(data.id) // data: User ✓ console.log(data.name) // ✓ }

Assertion Functions (Throw or Narrow)

function assertDefined<T>(val: T | null | undefined, msg: string): asserts val is T {
  if (val == null) throw new Error(msg)
}

const user = getUser(id) // User | null assertDefined(user, 'User must exist') console.log(user.name) // user: User ✓ (TypeScript knows it's non-null after assertion)

Truthiness Narrowing

function process(input: string | null | undefined | 0 | false) {
  if (input) {
    // input: string (all falsy values are excluded)
    console.log(input.toUpperCase())
  }
}
// Careful: '' (empty string) is also falsy — may not be what you want
// Use input != null for null/undefined only

Exhaustiveness Checking with never

type Shape = { kind: 'circle'; radius: number } | { kind: 'square'; side: number }

function area(shape: Shape): number { switch (shape.kind) { case 'circle': return Math.PI shape.radius * 2 case 'square': return shape.side ** 2 default: const _exhaustive: never = shape // ❌ Error if a new Shape is added without handling it throw new Error('Unhandled shape: ' + _exhaustive) } }

Practice TypeScript narrowing questions at [JSPrep Pro](/auth).

Put This Into Practice

Reading articles is passive. JSPrep Pro makes you actively recall, predict output, and get AI feedback.

Start Free →Browse All Questions

Related Articles

Deep Dive
We Built a RAG-Powered AI Question Engine Into a JavaScript Interview Platform — Here's Exactly How It Works
12 min read
Build Systems
Monorepo with Turborepo vs Nx: The Complete Comparison (2025)
9 min read
Core Concepts
map() vs forEach() in JavaScript: Which One to Use and Why It Matters
7 min read