React Forum
Blog & Guide Responses
Oct 01, 2022 · 04:00 AM

TypeScript: Narrowing - discussion (2022-10-01)

Started by Keira Santos
externaltypescriptnarrowingunionsguardsreact

TypeScript narrowing feels like it should be straightforward, but in UI code it’s always mixed with “what did the route promise?” and “what did the runtime actually provide?”.

Read the handbook

How do you structure UI surfaces so narrowing is obvious (discriminated unions, guard helpers, boundary assertions)? Do you narrow early in leaf components, or narrow once at the route/shell boundary and keep the rest of the tree boring? What narrowing patterns have actually reduced bugs for you (especially around optional props / undefined states)?

Comments (10)

Back to latest
Asha Rahman
Oct 01, 2022 · 04:06 AM

Discriminated unions for UI states (loading/ok/empty/error) are the highest ROI narrowing technique I’ve seen.

Dmitri Kline
Oct 01, 2022 · 04:14 AM

I’m biased toward narrowing at the boundary.

If the route contract says “this is OK-state”, components should not repeatedly re-prove it with if (!x) return null everywhere.

Related: TypeScript and Types and Core Concepts Best Practices.

Salma Qureshi
Oct 01, 2022 · 04:22 AM

A simple custom guard removes a lot of noise in components:

ts
type Ok<T> = { kind: 'ok'; value: T };
type Err = { kind: 'err'; message: string };
type Result<T> = Ok<T> | Err;

export function isOk<T>(r: Result<T>): r is Ok<T> {
  return r.kind === 'ok';
}

Then your UI reads like a contract: if ok, render value; if err, render message.

Noah Vance
Oct 01, 2022 · 04:31 AM

Counterpoint: boundary narrowing can hide reality when data is messy.

If you assert too aggressively, you can end up with runtime “undefined” that TypeScript promised couldn’t exist.

Keira Santos
Oct 01, 2022 · 04:36 AM

That’s fair. I think the boundary has to be both an assertion and an evidence marker.

If you assert, render a contract marker so you can see what you asserted on a given screen.

Noah Vance
Oct 01, 2022 · 04:39 AM

Yeah — otherwise the assertion is invisible and debugging becomes archaeology.

Hana Saito
Oct 01, 2022 · 04:47 AM

For props, I like making impossible states literally impossible.

If a component needs user, don’t accept user?: User. Make it a required prop and narrow earlier in the tree.

Priya Liao
Oct 01, 2022 · 04:56 AM

Optional chaining everywhere is a smell for me.

It often means we didn’t decide whether the value is required by the contract or truly optional in the UI.

Camille Duarte
Oct 01, 2022 · 05:07 AM

We use narrow UI surfaces even when runtime inputs are wide.

ts
type CardProps =
  | { mode: 'loading' }
  | { mode: 'empty'; message: string }
  | { mode: 'ok'; title: string; body: string };

No undefined props, no “maybe”. The render story is explicit.

Soren Malik
Oct 01, 2022 · 05:21 AM

A nice trick: when narrowing is hard, you probably need a better discriminant.

If you’re checking five fields to guess which shape you have, make a kind and move on.

Inez Park
Oct 01, 2022 · 05:32 AM

Narrowing mistakes show up as “can’t reproduce” UI bugs. Anything that makes the state shape explicit is worth it.

Benji Rios
Oct 01, 2022 · 05:44 AM

My least favorite pattern is narrowing via truthiness (if (x)) when x can be 0 or ''.

It’s an easy way to create bugs that TypeScript won’t warn you about because it assumes you meant it.