Settings
Your save file

Progress lives in this browser, on this device. Download the save file to move it to another device, or to keep a copy in case this browser clears its storage.

Season 2
Week 9 of 12

Reading time · 3 min

One comp · scored

Up next

Week 10 · The Power Shift

Locked until you win

Week 9 · Endgame

The Jury Speaks

Discriminated unions

loading | ready | error: the pattern that is half of professional React TS.

Somewhere in your React code right now, maybe in the favorites chart component on BBJ, three separate booleans are trying to tell one story: isLoading, data, error, each one settable on its own, each one supposedly implying something about the other two. Nothing stops isLoading from staying true after data lands. Nothing stops error and data from both being truthy at once, one stale, one fresh, your render function guessing which one to trust. That bug does not need a race condition to happen. It just needs one missing !error in one if statement.

Boolean soup vs one true state

Three independent booleans can combine into eight states. Your component only ever meant three of them: loading, ready, or errored. The other five are bugs waiting for the right timing to show up. A discriminated union collapses all of that back down to exactly the three states that were ever real, by making the state one value instead of three.

The ES6 you write

function renderFavorites({ isLoading, data, error }) {
  if (isLoading && !data && !error) return 'Loading...'
  if (data && !isLoading && !error) return `Top pick: ${data[0]}`
  if (error && !isLoading && !data) return `Error: ${error}`
  return 'Something is wrong with three booleans pretending to be one state.'
}

The TS version

type PollState =
  | { status: 'loading' }
  | { status: 'ready'; leaders: string[] }
  | { status: 'error'; message: string }
 
function renderPoll(state: PollState): string {
  switch (state.status) {
    case 'loading':
      return 'Loading...'
    case 'ready':
      return `Top pick: ${state.leaders[0]}`
    case 'error':
      return `Error: ${state.message}`
    default: {
      const impossible: never = state
      return impossible
    }
  }
}

What changed

Three independent booleans become one object with a status field. isLoading, data, and error could previously disagree with each other; PollState physically cannot, because there is only one value to read, and its status field says which of the three shapes it is. Every if built from ANDs and NOTs becomes one case in a switch.

Notice there is no fourth if branch guessing what to do when the booleans disagree, because disagreeing is no longer possible. PollState is always exactly one of the three shapes, never a blend of two, never none of them.

Narrowing on the discriminant

status is the one field every variant shares, and it is the only field the switch needs to look at. The moment a case 'ready': label matches, TypeScript does not just know state.status is the string 'ready', it knows the entire state object narrowed down to the { status: 'ready'; leaders: string[] } variant, because status is a literal type that only one branch of the union could have produced. That is why state.leaders reads clean inside that case, no optional chaining, no cast, nothing. Step outside that case and leaders does not exist on the type at all, and TypeScript will say so the moment you try to read it.

Exhaustiveness: the never trick, an old friend from week 3

You met this move back in week three, checking a plain union of literal strings. It works exactly the same way here, just aimed at a switch over an object's discriminant instead of a bare string. Handle 'loading', 'ready', and 'error', and the default branch has nothing left to be. Assign whatever is left to a variable typed never, and if a fourth state ever gets added to the union without a matching case, that assignment stops compiling: a red squiggle, right at the keyboard, long before the missing case ships and somebody's favorites chart quietly renders nothing on air.

That is the whole lesson. Now prove it in the comp below.

Up next

Week 10 · The Power Shift

Utility types

Locked until you win