[Deep Dive #9] TypeScript Boundaries - discussion
When you say 'TypeScript boundaries', where do you actually draw that line in a real app (route shell, helper layer, screen components)?
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 latestWhat is a 'boundary' in this context?
Is it just API calls? Or component boundaries too?
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.
Ah, ok. I read "boundary" as "fetch layer" only.
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.
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.
"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.
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.
Strict docs in prototypes feels like dieting.
We tag keys as experimental in the doc surface and move on until the surface settles.
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.
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.
Tiny example: normalize and store a derived shape once.
ts
doc.write('userModel', { id: String(raw.id), name: raw.name ?? 'Unknown' });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.
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.
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.
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.
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.