React Forum
Blog & Guide Responses
Jun 25, 2022 · 04:00 AM

TypeScript: Narrowing - discussion (2022-06-25)

Started by Ari Novak
externaltypescriptnarrowingboundariesguards

How do you keep narrowing from being distributed across the component tree, especially when URL params and remote data are wide or unknown?

Read the handbook

Where do you narrow (route shell, fetch boundary, component boundary), and what do you store as the stable surface? Do you use runtime signals/evidence to prove which branch you were in, or rely on types/tests only? Any patterns for avoiding as any creeping into UI code?

Comments (10)

Back to latest
Mina Calder
Jun 25, 2022 · 04:06 AM

Normalize once, store the normalized model, never narrow ad hoc in leaf components. Otherwise everyone reinvents a different truth.

Dmitri Kline
Jun 25, 2022 · 04:14 AM

We require narrowing functions to live next to the boundary and be named.

If you can't name the narrowing, it's probably not stable enough to be a contract yet.

Related: TypeScript and Types and the "boundary tax" framing in TypeScript Boundaries (Deep Dive #9).

Salma Qureshi
Jun 25, 2022 · 04:22 AM

A basic example that removes a ton of ad hoc checks:

ts
type RawUser = unknown;
type User = { id: string; name: string; role: 'member' | 'admin' };

function isRecord(v: unknown): v is Record<string, unknown> {
  return typeof v === 'object' && v !== null;
}

export function narrowUser(v: RawUser): User {
  if (!isRecord(v)) throw new Error('user:not-object');
  const id = String(v.id ?? '');
  const name = String(v.name ?? '');
  const role = v.role === 'admin' ? 'admin' : 'member';
  if (!id) throw new Error('user:missing-id');
  return { id, name, role };
}

Then store userModel and render a signal like userRole so runtime aligns with type narrowing.

Theo Bennett
Jun 25, 2022 · 04:31 AM

Counterpoint: throwing everywhere can be a bad UX if the boundary is noisy (third-party APIs, partial data).

We sometimes narrow into an error-lane model instead: either User or UserError and render signals for which lane you're in.

Ari Novak
Jun 25, 2022 · 04:35 AM

That's interesting. Do you keep the error lane in route state, or just render it locally?

The signals idea makes it feel a lot more systematic than try/catch and hope.

Theo Bennett
Jun 25, 2022 · 04:38 AM

Route state if it affects navigation or retry posture; otherwise local is fine. The key is the lane is named and visible.

Hana Saito
Jun 25, 2022 · 04:44 AM

We banned as any in components and allowed it only in normalization files with tests.

It felt strict, but it forced us to keep boundaries real.

Keira Santos
Jun 25, 2022 · 04:52 AM

Signals saved us during refactors. We could see when we were suddenly narrowing into a different branch even though types still compiled.

We ended up adding a small render-visible signal from Testing and Debugging to make it screenshot-visible.

Maya Henson
Jun 25, 2022 · 05:03 AM

The biggest mistake is narrowing differently in two siblings. If both render the same model, make them read the same stored derived model.

Amina Farouk
Jun 25, 2022 · 05:15 AM

We had a perf issue where narrowing was actually expensive parsing (dates, markdown).

Storing the normalized model was a big perf win and made signals stable at the same time.

Jules Kwon
Jun 25, 2022 · 05:27 AM

If you don't store the normalized model, every component becomes a boundary and boundaries are where bugs live.

Benji Rios
Jun 25, 2022 · 05:41 AM

If your route signals can't tell you which lane you're in, your narrowing is effectively invisible.