React Forum
Blog & Guide Responses
Jan 26, 2023 · 04:00 AM

TypeScript: Narrowing - discussion (2023-01-26)

Started by Avery Chen
externaltypescriptnarrowingguardsdiscriminated unionsassertsintent objectslanes

Narrowing is the difference between "we have types" and "the UI can't lie". Once you start treating routes and mutation intent as contracts, the best patterns are the ones that make illegal states unrepresentable *and* keep evidence rendering readable. I'd love to hear what narrowing moves people actually ship in React codebases.

Read the handbook

Do you model async lanes as a discriminated union or keep parallel booleans around? How do you keep route params typed without resorting to as casts everywhere? What do you prefer for runtime boundaries: custom type guards, asserts, or schema validation helpers? Any patterns for narrowing event payloads / currentTarget in a way that's still ergonomic?

Comments (16)

Back to latest
Marisol Vega
Jan 29, 2023 · 09:30 PM

Discriminated unions for lanes. Every time.

I used to think it was overkill until I had to explain "loading + error" simultaneously in a bug report.

Dmitri Kline
Feb 06, 2023 · 06:42 AM

We type the lane and the evidence together so the UI is forced to be honest:

ts
type Lane =
  | { kind: 'idle' }
  | { kind: 'pending'; startedAt: number }
  | { kind: 'error'; message: string; retryAfterMs?: number }
  | { kind: 'ok'; value: unknown; receivedAt: number };

function renderLaneEvidence(l: Lane) {
  if (l.kind === 'pending') return `pending:${l.startedAt}`;
  if (l.kind === 'error') return `error:${l.message}`;
  if (l.kind === 'ok') return `ok:${l.receivedAt}`;
  return 'idle';
}

Then we render data-lane={renderLaneEvidence(lane)} on the shell.

Keira Santos
Feb 15, 2023 · 02:05 PM

Counterpoint: unions are great until you start nesting them for every tiny widget and you can't read the types anymore.

We keep unions for anything that crosses routes or is persisted; local ephemeral stuff stays lightweight.

Avery Chen
Feb 18, 2023 · 09:20 AM

That threshold makes sense.

If you can't explain the union shape out loud, it's probably not a contract; it's just noise.

Grace McCoy
Feb 28, 2023 · 04:50 AM

We use asserts at the route boundary to avoid cascading casts:

ts
function assertString(v: unknown, label: string): asserts v is string {
  if (typeof v !== 'string') throw new Error(`${label}: expected string`);
}

Not glamorous, but it means the rest of the code doesn't have to pretend.

Jasper Nguyen
Mar 12, 2023 · 10:22 PM

I like narrowing by stable discriminants (kind/type) because it also improves logging.

When a bug comes in, you can paste the object and immediately see which lane it is.

Nina Kapoor
Mar 29, 2023 · 08:40 AM

Event narrowing: I mostly use instanceof on FormData / URLSearchParams boundaries rather than trying to over-type DOM events.

Typing currentTarget perfectly is less valuable than keeping handler paths explicit.

Camille Duarte
Apr 16, 2023 · 11:11 AM

One small win: treat intent objects as a union and narrow in the mutation handler.

It keeps the "why did this happen" story tight, and it matches the evidence posture from these guides.

Owen Price
May 07, 2023 · 05:07 AM

We got a lot of leverage out of narrowing query params, especially for filter panels.

If you don't narrow, every panel becomes string | null | undefined soup.

Benji Rios
May 26, 2023 · 07:19 PM

Counterpoint: narrowing can give a false sense of safety if any sneaks in through JSON boundaries.

If the boundary isn't validated, the type checker is just being polite.

Lina Ortega
Jun 02, 2023 · 03:30 AM

Yep. This is why I like asserts helpers right after parsing and before storing anything.

Once it's in a vault/store, you want it to be "typed forever".

Sofia Valdez
Jun 20, 2023 · 12:40 PM

We use unions for route keys too:

ts
type Panel = 'overview' | 'details' | 'debug';
type RouteState = { panel: Panel; focus?: 'search' | 'composer' };

It's basic, but it prevents "mystery panel" bugs.

Theo Bennett
Jul 09, 2023 · 04:44 AM

We still do some casts, but the rule is: only cast in adapters.

If a component needs as, the boundary is in the wrong place.

Amina Farouk
Aug 02, 2023 · 10:10 PM

Narrowing pairs really well with the docs' TypeScript + patterns pages.

Related: TypeScript and Types and Patterns Library.

Tomas Rivera
Aug 28, 2023 · 08:08 AM

My favorite narrowing pattern is boring: optional chaining + early returns.

It keeps handlers short and you don't end up with giant if pyramids.

Salma Qureshi
Sep 22, 2023 · 04:16 PM

For me the win isn't "type safety"; it's refactor safety.

When you tighten a union, the compiler tells you every UI lane you forgot to update.

Rowan Pierce
Oct 19, 2023 · 06:06 AM

If you're doing async flows, consider narrowing + evidence as one story: model the flow, render the flow.

Otherwise people end up staring at spinners with no idea what's happening.

Priya Liao
Dec 06, 2023 · 03:03 AM

Minor take: TS narrowing is also a UX tool. It forces you to decide what states are possible.

When you decide that, it's easier to design UI that doesn't contradict itself.