TypeScript: Narrowing - discussion (2023-06-03)
The Narrowing chapter is where TypeScript starts acting like a policy engine: your if statements and type predicates become part of your app's runtime behavior. In React apps, the question I care about is where narrowing decisions live so they don't drift across surfaces.
Do you prefer type guards/type predicates, or normalize into a contract object and avoid guards downstream? Where do you put narrowing logic: route docs, shared helpers, or at the leaf components? Do you log narrowing outcomes for user-facing boundaries (URLs/forms) so debugging is possible from a report?
Comments (12)
Back to latestNormalize once at the boundary.
Then components never parse or narrow.
We log narrowing outcomes for URLs because it's the only way to debug weird shared links:
txt
[tips] narrow=query.page raw="-1" result=clamp value=1 reason=outOfRange
[tips] narrow=query.sort raw="" result=drop reason=empty
Narrowing isn't just for TS; it's product behavior.
Type guards are fine if they're tiny and centralized.
What breaks is when each component narrows slightly differently and you get inconsistent behavior across routes.
Concrete alternative: use asserts to fail fast in dev and fallback in prod.
If you only fallback, you may never notice the boundary drift until users do.
This is what we do too: throw in dev, lane=error + evidence in prod.
Long-form: narrowing is a drift policy.
If you normalize query params, you are picking winners: clamp vs drop vs error lane. Those decisions should be explicit and reviewable, not embedded in random Number() calls.
Docs tie-in: narrowing pairs with routing/state docs when you treat boundaries as contracts.
Related: TypeScript and Types and Routing and Navigation.
Short take: the best narrowing is the one you don't have to repeat.
We banned parsing in components and it solved most inconsistencies.
Route docs parse, normalize, and then the rest of the app consumes the contract shape.
Counterpoint: local guards are fine for local state, not for boundary state.
If it's coming from URL/network/user input, narrowing should be centralized.
We write unit tests for normalization rules. It's cheap and it prevents subtle regressions.
If narrowing is product behavior, it deserves tests.
If you change narrowing behavior, log it. Otherwise link debugging becomes archaeology.
Takeaway: narrowing is where types become policy. Treat it as a boundary contract and keep it observable.
If you keep it implicit, you'll end up with inconsistent behavior that types can't save you from.