TypeScript: Narrowing - discussion (2023-01-02)
Narrowing is one of those TS topics that only feels important after you have shipped a bug caused by assuming a value was present. In UI code it shows up constantly: optional props, nullable server fields, and route params that may or may not exist depending on navigation posture.
Do you write explicit type guards in UI code, or rely on ad-hoc checks inline? Where do you prefer to narrow: at the route boundary, inside components, or in a shared adapter layer? How do you keep narrowing from turning into a maze of conditions as the UI grows?
Comments (10)
Back to latestI narrow at the boundary if I can. It keeps components boring.
Type guards are worth it when the same narrowing logic repeats across surfaces.
Otherwise, you end up with five slightly different checks and they drift.
Related: TypeScript Boundaries (Deep Dive #9).
Example guard we use a lot for route params:
ts
type Params = { id?: string };
export function hasId(p: Params): p is { id: string } {
return typeof p.id === 'string' && p.id.length > 0;
}
Then the route can decide whether to redirect, render a missing state, or open a bridge flow.
Counterpoint: guards can become a new abstraction layer that hides simple checks.
If it is only used once, I would rather see the check inline.
Agreed. I reach for guards when they encode policy (what counts as valid) rather than just syntax sugar.
If the guard is the policy, it belongs at the boundary.
Policy vs convenience is the right split.
We also use discriminants as a narrowing tool for UI state, and it pairs well with rendering evidence.
If the discriminant is visible (even as a data attribute), debugging becomes much less guessy.
A misunderstanding in my team: people thought if (value) was a valid guard for everything.
It breaks down fast with empty strings, zero, and deliberate falsy values.
For UI, the best narrowing is often turning unknowns into a stable route shape early.
Then components only render what the route contract guarantees.
I like guards for server payloads too. Narrow once, then store a derived-ready shape in the document so rendering stays consistent.
We got rid of a bunch of UI edge bugs by narrowing in one adapter function and making the adapter the only place we touched raw payloads.
It also made the codebase easier for new hires.
Narrowing is basically contract enforcement. If you care about route contracts, you should care about narrowing.