Lesson 10 of 13 · 0 of 1 exercises solved

Narrowing & Type Guards

Learn how TypeScript narrows a such as | number to a single type by following your if statements and checks. This is how you work safely with values that can be more than one type.

function format(value: string | number | Date): string {
  // typeof narrowing
  if (typeof value === 'string') {
    return value.toUpperCase()      // value: string here
  }
  if (typeof value === 'number') {
    return value.toFixed(2)         // value: number here
  }
  // instanceof narrowing
  return value.toISOString()        // value: Date — the only option left
}

// truthiness narrowing
function greet(name?: string) {
  if (!name) return 'Hello, stranger'
  return `Hello, ${name.trim()}`     // name: string
}

// "in" narrowing for object shapes
type Bird = { fly: () => void }
type Fish = { swim: () => void }

function move(animal: Bird | Fish) {
  if ('fly' in animal) animal.fly()
  else animal.swim()
}
 
TypeScript (compiler loads on first run)Ctrl+Enter runs
// Discriminated unions — the single most useful modeling pattern in TS.
// Every member shares a literal "tag" field the compiler can switch on.
type Result<T> =
  | { status: 'loading' }
  | { status: 'success'; data: T }
  | { status: 'error'; error: Error }

function render(result: Result<string[]>): string {
  switch (result.status) {
    case 'loading': return 'Spinner...'
    case 'success': return result.data.join(', ')   // .data exists only here
    case 'error':   return result.error.message     // .error exists only here
  }
}

// Exhaustiveness checking: if someone adds a new variant, this fails to compile.
function assertNever(x: never): never {
  throw new Error('Unhandled case: ' + JSON.stringify(x))
}

function renderStrict(result: Result<string[]>): string {
  switch (result.status) {
    case 'loading': return 'Spinner...'
    case 'success': return result.data.join(', ')
    case 'error':   return result.error.message
    default:        return assertNever(result)
  }
}
 
TypeScript (compiler loads on first run)Ctrl+Enter runs
// Custom type guards: a function whose return type is "arg is T"
type User = { id: number; email: string }

function isUser(value: unknown): value is User {
  return (
    typeof value === 'object' && value !== null &&
    'id' in value && typeof (value as User).id === 'number' &&
    'email' in value && typeof (value as User).email === 'string'
  )
}

async function loadUser(): Promise<User> {
  const data: unknown = await fetch('/api/me').then(r => r.json())
  if (!isUser(data)) throw new Error('API returned an unexpected shape')
  return data                       // data: User from here on
}

// Assertion functions throw instead of returning a boolean
function assertDefined<T>(v: T | null | undefined, name: string): asserts v is T {
  if (v == null) throw new Error(name + ' is required')
}

function process(input?: string) {
  assertDefined(input, 'input')
  return input.length               // input: string — no ? needed
}
 
TypeScript (compiler loads on first run)Ctrl+Enter runs

Practice

Write a type guard isString(value: unknown): value is string, then formatValue(value: unknown): string that returns strings upper-cased, numbers with two decimals, as their length in brackets, and "?" for anything else — narrowing at each step.

Exercise

Narrowing unknown

3 checks
function isString(value) {
  return false
}

function formatValue(value) {
  return "?"
}

console.log(formatValue("hello"), formatValue(3.14159), formatValue([1, 2, 3]), formatValue(null))
 
TypeScript (compiler loads on first run)Ctrl+Enter runs
Output
Press Run to try your code, then Check answer to grade it.

Check yourself

Quick check

Why prefer unknown over any for a value you have not inspected yet?

Finished reading?1 exercise still open. You can come back to it later.

Tip: use the ← and → keys to move between lessons.