TypeScript: Narrowing - discussion (2023-01-14)
TS narrowing is one of those topics that seems "language-y" until you start modeling UI state lanes and intent objects. I'm curious what narrowing patterns people rely on most in React code, especially when the goal is to keep state transitions and evidence rendering honest.
Do you use discriminated unions for UI lanes (pending/ok/error), or do you keep separate booleans? What narrowing patterns keep reducers/intent handlers readable? Have you used custom type guards in app code, or do you mostly stick to in / typeof / instanceof?
Comments (12)
Back to latestDiscriminated unions for lanes are the biggest win. Booleans inevitably drift.
Reducer readability improves when the union is the *only* input shape you accept:
ts
type Lane = { kind: 'idle' } | { kind: 'pending'; startedAt: number } | { kind: 'error'; message: string } | { kind: 'ok' };
function renderLane(l: Lane) {
if (l.kind === 'pending') return `pending:${l.startedAt}`;
if (l.kind === 'error') return `error:${l.message}`;
return l.kind;
}
Now your evidence rendering is type-checked too.
Counterpoint: unions can get annoying if you over-model every little thing.
We keep unions for state machines and keep the rest simple.
Same. The threshold for me is: do we need this to be observable and replayable?
If yes, model it. If no, keep it lightweight.
Custom type guards show up mostly at boundaries (API payload validation).
Inside the app I mostly use discriminants and in checks.
A pattern we use: prefer narrowing by a stable type or kind field over typeof checks.
It makes logs and evidence consistent too.
This also helps with "route contracts" in these sites.
If route keys are unions, you can keep UI motion explicit without allowing arbitrary strings.
We still have a couple of isLoading booleans and they always end up in impossible combinations.
Every time we replace one with a union lane, the code gets calmer.
Docs tie-in: narrowing is a good companion to the "evidence" posture.
If the state is modeled, your evidence rendering becomes correct-by-construction.
Related: API Reference and Testing and Debugging.
One mistake: people use narrowing but still allow any to enter through JSON boundaries.
If you don't validate the boundary, narrowing isn't saving you.
I like the handbook's emphasis that narrowing is just "making the type more specific".
In UI code that often means: pick one lane and commit to it, rather than juggling booleans.
We also use narrowing patterns in event handlers (currentTarget guards, optional chaining) to avoid a bunch of runtime checks.
Not glamorous, but it keeps UI code tidy.
State lanes + evidence rendering is where narrowing feels most valuable to me. It's not just type safety; it's UX clarity.