React Forum
Blog & Guide Responses
Jun 07, 2022 · 04:00 PM

[Deep Dive #9] TypeScript Boundaries - discussion

Started by Harper Iqbal
blogtypescriptboundariessurface contractsderived shapesprops

When you say 'TypeScript boundaries', where do you actually draw that line in a real app (route shell, helper layer, screen components)?

Read the post

I read this as: boundaries are where you stabilize truth. Wide inputs are fine if you store derived shapes at the boundary and render proof (evidence keys) so the UI doesn't silently depend on ad hoc narrowing.

Where do you define the boundary (route shell, screen docs, helper layer)? Do you store narrowed/derived shapes in documents to avoid repeated narrowing? How strict are you about 'no ad hoc keys' in documents/props? And how do you keep boundary types aligned when the file router map changes?

Comments (10)

Back to latest
Maya Henson
Jun 07, 2022 · 04:05 PM

What is a 'boundary' in this context?

Is it just API calls? Or component boundaries too?

Harper Iqbal
Jun 07, 2022 · 04:10 PM

Component boundaries too—especially where route truth enters a screen.

Anywhere you go from "wide" data to "stable surface" data is a boundary worth naming and making visible.

Maya Henson
Jun 07, 2022 · 04:13 PM

Ah, ok. I read "boundary" as "fetch layer" only.

Benji Rios
Jun 07, 2022 · 04:18 PM

On a larger team, storing derived shapes at boundaries reduces incidental complexity.

If each component narrows differently, you end up debugging types by reading runtime behavior.

Rina Kobayashi
Jun 07, 2022 · 04:26 PM

Repeated narrowing can also hide repeated heavy parsing (dates, markdown, etc.).

We store parsedAt or normalizedModel on the doc so downstream components don't redo it.

Sofia Valdez
Jun 07, 2022 · 04:35 PM

"No ad hoc keys" is huge for route docs.

If you keep documents strict, you can refactor the UI motion without migrating data models every time.

Theo Bennett
Jun 07, 2022 · 04:40 PM

Agreed. It's easy to add a key for convenience, but it becomes a surface accidentally.

If it's useful to render, it's useful to store—but name it and own it.

Jasper Nguyen
Jun 07, 2022 · 04:48 PM

Strict docs in prototypes feels like dieting.

We tag keys as experimental in the doc surface and move on until the surface settles.

Amina Farouk
Jun 07, 2022 · 04:56 PM

We hit this in prod: one panel treated id as string and another treated it as number (both compiled fine due to any at the boundary).

Once we added a boundary normalization step and stored the normalized shape, the class of bug disappeared.

Mina Calder
Jun 07, 2022 · 05:02 PM

This is exactly why storing derived shapes is useful: it makes the boundary decision durable.

You don't have to rediscover the same narrowing logic in every component.

Lena Fischer
Jun 07, 2022 · 05:12 PM

Tiny example: normalize and store a derived shape once.

ts
doc.write('userModel', { id: String(raw.id), name: raw.name ?? 'Unknown' });
Rowan Pierce
Jun 07, 2022 · 05:24 PM

Question: how do you keep boundary types aligned with routing changes?

We had a file route refactor that changed params and it cascaded into types in weird places.

Camille Duarte
Jun 07, 2022 · 05:36 PM

We keep param normalization in the route shell and store the normalized params as derived keys.

Then screens depend on the derived params, not on raw URL params, and refactors stay localized.

Soren Malik
Jun 07, 2022 · 05:50 PM

Wide inputs are fine, but only if you pay the boundary tax once and write it down (in code + docs).

Otherwise every component ends up doing its own little "helpful" narrowing.

Dmitri Kline
Jun 07, 2022 · 06:03 PM

A concrete rule we adopted: the route shell is the only place allowed to convert "unknown" into "stable". Everything below the shell consumes a narrow surface type and isn't allowed to sprinkle as any to keep moving.

ts
type RawParams = { id?: string | string[]; mode?: string };
type StableParams = { id: string; mode: 'view' | 'edit' };

function normalizeParams(p: RawParams): StableParams {
  const id = Array.isArray(p.id) ? p.id[0] : p.id;
  const mode = p.mode === 'edit' ? 'edit' : 'view';
  if (!id) throw new Error('missing id');
  return { id, mode };
}

Then we store the normalized params on the doc (as derived keys) and render an evidence key like paramEvidence={id,mode} so refactors don't create silent behavior changes.

Hana Saito
Jun 07, 2022 · 06:15 PM

The boundary I regret not naming sooner was date parsing. Two components parsed the same ISO string differently and we chased it for days.

Once we normalized at the boundary and stored normalizedModel, the bug disappeared and perf improved as a side effect.