webroad.online
  1. 1Web
  2. 2HTML
  3. 3CSS
  4. 4JavaScript
  5. 5TypeScript
  6. 6Git
  7. 7Tooling
  8. 8React
  9. 9State management
  10. 10Next.js
  11. 11Forms
  12. 12Data and backend
  13. 13SEO
  14. 14Tailwind CSS
  15. 15Animations
  16. 16Testing
  17. 17Architecture
TypeScript · Lesson 3 of 6

Union types and narrowing

Unions, literal types, typeof/in/instanceof, discriminated unions and never.

Updated

What a union type is

A union says: the value can be one of several types.

type Id = string | number
type Status = 'idle' | 'loading' | 'success' | 'error'

It's one of the most used tools in TS: statuses, component variants, responses that can be a success or an error.

What narrowing is

If a value is string | number, TS only lets you use what both have in common. To use string methods, you have to prove it's a string. That's narrowing: an if after which TS knows the exact type.

function format(id: string | number) {
  id.toUpperCase()                 // ✗ number has no toUpperCase
  if (typeof id === 'string') {
    return id.toUpperCase()        // ✓ here TS knows: string
  }
  return id.toFixed(0)             // ✓ here: number
}

Kinds of narrowing — and when

Check For Example
typeof x === 'string' primitives string | number
x === null, x !== undefined, if (x) missing values User | null
'key' in x objects with different shapes Cat | Dog
x instanceof Error classes catch (e)
Array.isArray(x) an array vs something else string | string[]
x.kind === 'circle' discriminated unions see below
an x is T function a reusable check isUser(x)

Discriminated unions — the most useful pattern

Each variant has a common field with a different literal value (the discriminant). A switch on it narrows automatically:

type Result =
  | { status: 'success'; data: User }
  | { status: 'error'; message: string }
  | { status: 'loading' }

function render(r: Result) {
  switch (r.status) {
    case 'success': return r.data.name      // TS knows data exists
    case 'error': return r.message          // here, message
    case 'loading': return 'Loading…'
  }
}

It's impossible to read data in an error state — a whole class of bugs disappears. The same pattern describes a reducer's actions in React.

Exhaustive checks with never

If you later add { status: 'empty' }, you want TS to show you every switch that doesn't handle it:

default: {
  const unreachable: never = r      // ✗ an error if a variant is left unhandled
  return unreachable
}

Type predicates

A function that checks a type and "tells" TS the result:

function isString(x: unknown): x is string {
  return typeof x === 'string'
}

const values: unknown[] = ['a', 1, 'b']
const strings = values.filter(isString)   // string[]

Summary

  • A union = "one of"; literal unions for statuses and variants.
  • Narrowing = typeof, in, instanceof, equality — after the if, TS knows the type.
  • Discriminated unions + switch + never = code without impossible states.

Official sources

Exercises

Was this page helpful?

One tap — no account needed.