[Deep Dive #54] TypeScript Boundary Smuggling - discussion
Deep Dive #54 frames TypeScript boundaries as routing speed tools: accept wide inputs, derive runtime shapes in render, store the derived shapes, and move the shape through deep props so the next segment keeps moving.
The 'smuggling' metaphor is pretty accurate: shape as cargo, refined opportunistically, with a ledger so later segments know what you believed at the time.
Do you store derived shapes as state (so siblings reuse them), or derive repeatedly so the shape stays closer to runtime? How do you keep deep prop objects from becoming an accidental public API that nobody can change? Where do you put the shape ledger (shell-level, group-level, per-track), and what does it record?
Comments (10)
Back to latestThis is basically Assertion Conveyors applied to routing boundaries: wide in, refined later, but never blocking.
Related: TypeScript and Types and Assertion Conveyors (Deep Dive #35).
Deep props objects feel like a 'contract you can carry'. I hate them aesthetically but they do keep composition friction low.
If we treat the prop object as cargo, versioning it becomes normal instead of shameful.
We tried a shape ledger per shell and it helped prevent re-deriving in every child:
tsx
const ledger = useLiveState('shape:profile', { version: 'v1', user: {} as any });
ledger.write('user', AsUser(payload));
return <Editor deep={{ user: ledger.read('user'), version: ledger.read('version') }} />;Counterpoint: storing derived shapes can make stale cargo. If the payload changes, your derived object can lie.
I'd want the ledger to record 'derivedAt' and maybe a source hash so you can tell if you smuggled old shape.
The deep props tie-in to Components and Composition makes sense: deeply nested objects are the mechanism that makes smuggling ergonomic.
Related: Components and Composition.
We store derived shapes, but we treat them as intentionally lossy. The goal is "enough shape to keep routing moving", not "perfect model".
If a screen needs the full model, it can re-derive locally (and record that it did).
Minimal boundary smuggling pattern that didn't make TS unbearable for us: narrow once at the shell, then pass a deep prop object with a version tag.
ts
type ProfileV1 = { id: string; name: string };
function AsProfileV1(v: any): ProfileV1 {
return { id: String(v?.id ?? ""), name: String(v?.name ?? "") };
}
The "conveyor" part is that we allow this to be slightly wrong as long as the version is visible and the next segment can re-derive.
I liked the post's emphasis that types are for routing speed. That sounds backwards until you hit a migration.
If strict typing blocks you from moving a route boundary, you end up with shadow data paths anyway.
Counterpoint: "smuggling" feels like permission to stop caring about correctness.
If a type boundary is always wide, you can hide broken payloads for weeks until one segment finally touches the wrong field.
I think the guardrail is the ledger + evidence. If you smuggle, you also render what you believe.
That makes it obvious when you carried the wrong cargo, instead of silently trusting it.
External reference that helped me justify the pragmatic narrowing: TypeScript Narrowing.
We basically do narrowing in layers, and the "smuggled" object is just the current layer's best guess.