TypeScript Narrowing & Discriminated Unions (Interview-Ready)

A deep, interview-ready guide to narrowing in TypeScript: typeof, in, instanceof, user-defined type guards, and discriminated unions for safe, expressive code.

F

Frontend Interview Team

February 08, 2026

~24 min
TypeScript Narrowing & Discriminated Unions (Interview-Ready)

Narrowing is how TypeScript lets you safely work with unions.

Interviewers love this topic because it shows whether you can write safe code without overusing type assertions.


1) What is narrowing?

If a value has a union type:

type Id = string | number;

TypeScript needs checks to decide which branch you’re in.


2) typeof narrowing

function format(id: string | number) {
  if (typeof id === 'string') {
    return id.toUpperCase();
  }
  return id.toFixed(2);
}

3) in operator narrowing

type A = { a: string };
type B = { b: number };
 
type X = A | B;
 
function f(x: X) {
  if ('a' in x) {
    return x.a;
  }
  return x.b;
}

4) instanceof narrowing

function f(err: unknown) {
  if (err instanceof Error) {
    return err.message;
  }
  return String(err);
}

5) User-defined type guards

type User = { id: string; name: string };
 
function isUser(x: any): x is User {
  return x && typeof x.id === 'string' && typeof x.name === 'string';
}
 
function handle(x: unknown) {
  if (isUser(x)) {
    x.name; // typed
  }
}

This is a strong interview pattern.


6) Discriminated unions (most important)

Discriminated unions use a shared "tag" field.

type ApiResult<T> =
  | { ok: true; data: T }
  | { ok: false; error: string };
 
function handle<T>(res: ApiResult<T>) {
  if (res.ok) {
    return res.data;
  }
  return res.error;
}

Because ok is the discriminant, TS narrows automatically.

Another example

type Shape =
  | { kind: 'circle'; radius: number }
  | { kind: 'square'; size: number };
 
function area(s: Shape) {
  switch (s.kind) {
    case 'circle':
      return Math.PI * s.radius * s.radius;
    case 'square':
      return s.size * s.size;
  }
}

7) Exhaustiveness checking

Add a never check to ensure all cases are handled.

function assertNever(x: never): never {
  throw new Error('Unhandled case');
}
 
function area(s: Shape) {
  switch (s.kind) {
    case 'circle':
      return Math.PI * s.radius * s.radius;
    case 'square':
      return s.size * s.size;
    default:
      return assertNever(s);
  }
}

Interviewers love this.


Summary

  • Narrowing makes unions safe.
  • Use typeof/in/instanceof.
  • Prefer discriminated unions for structured results.
  • Use never to enforce exhaustiveness.