TypeScript: Narrowing - discussion (2023-02-08)
Narrowing is where TypeScript stops being decoration and becomes a UI correctness tool. When you model lanes and intent objects, narrowing keeps reducers and handlers honest: you can't "forget" a state without the compiler making noise. I'm curious what narrowing patterns people actually rely on day-to-day.
Do you use discriminated unions for async lanes and intent objects, or keep separate flags? What narrowing patterns keep complex handlers readable (kind, type guards, asserts)? Do you treat narrowing as part of your evidence/logging posture (typed vocab -> stable logs)?
Comments (12)
Back to latestDiscriminated unions for lanes is the biggest win. Booleans always drift into impossible combinations.
We narrow by kind and then render evidence from the narrowed shape. It makes UI truth and type truth line up:
ts
type Lane = { kind: 'pending'; reason: string } | { kind: 'ok'; freshnessAt: string } | { kind: 'error'; message: string };
function laneEvidence(l: Lane) {
if (l.kind === 'pending') return `pending:${l.reason}`;
if (l.kind === 'ok') return `ok:${l.freshnessAt}`;
return `error:${l.message}`;
}
And the log lines become stable because they share the same vocabulary:
txt
[tips] lane=pending reason=route:enter
[tips] lane=ok freshnessAt=2023-02-14T14:14ZCounterpoint: unions can get unreadable if you over-model everything.
We keep unions for state machines and keep the rest as plain objects. Otherwise types become their own app.
Same threshold: if it crosses boundaries (route, persistence, panels), model it. If it's local and ephemeral, keep it simple.
Narrowing is a tool, not a lifestyle.
asserts helpers are underrated for runtime boundaries (query params, JSON).
If you don't validate the boundary, narrowing doesn't protect you.
Long-form: narrowing is also a readability tool because it forces you to pick a vocabulary.
Once you pick a vocabulary (kind, intent.type, posture), you can align UI evidence and logs with it. That alignment is what makes large systems debuggable.
We use narrowing for event handlers too, but only lightly (early returns).
Trying to perfectly type DOM events made the code worse than runtime checks.
Counterpoint: if you type and narrow everything, people start using as casts to escape.
We keep types small and let narrowing be shallow. Otherwise the system becomes self-defeating.
We model route keys as unions and it prevented "mystery panel" bugs.
When the panel is typed, the UI can render evidence and tests can assert it reliably.
Long-form counterpoint: the biggest risk isn't missing a case, it's having too many cases that mean the same thing.
If you have 7 lanes but only 3 UI behaviors, you're modeling internal implementation, not product truth.
Docs tie-in: narrowing + evidence posture is where TS becomes a product quality lever.
Related: TypeScript and Types and Testing and Debugging.
Narrowing forces teams to agree on states. That's the real value.
Once states are agreed, you can design UI and logs that actually explain the system.
If you're not sure where to start: model your async lane as a union and write one function that renders evidence from it.
That one move improves code review, debugging, and tests immediately.