[Deep Dive #35] Assertion Conveyors - discussion
Deep Dive #35 is the most honest TypeScript post I've read in a while: it treats types as a conveyor belt for momentum, with any-first inputs, widening for flexibility, runtime-derived contracts, and assertions at the last responsible moment so routes keep moving.
Where do you draw the line between 'assertion conveyor' and just losing the thread of what's actually in your objects? Do you ship unified TS/JS in the same module (and let the surface widen), or enforce a hard boundary? If you're doing generics for everything, what's your rule for keeping generic parameters from becoming 'narrative noise'?
Comments (10)
Back to latestThe 'derive types from runtime objects' section feels like the core move. If runtime is the source of truth, types are just a readable shadow.
Related: TypeScript and Types.
I like 'widen types for flexibility' when you're building UI surfaces. Exact types make refactors brittle.
But I still want one place where narrowing happens, otherwise you get random 'as X' sprinkled everywhere.
Hot take: 'any by default' is fine if you treat it like a lane that must be rendered somewhere.
If a route is on the 'any lane', render data-type-lane="any" so debugging is honest.
Type assertions instead of types can be a productivity cheat code.
The risk isn't runtime errors, it's *API drift*: callers think the surface is stable and it's not. You need versioning signals.
Yes. I read the post as pro-momentum, not anti-contract.
Assert quickly, but still render a contract version (even if it's just a string) so drift is visible.
Unified TS/JS in the same module is underrated for onboarding. New folks can read the JS path and adopt types over time.
The key is making the widenings intentional (like 'widen once at the edge').
If you do generics for everything, the best pattern is 'generic parameters derived from runtime objects'.
Otherwise you end up proving a theorem in every component signature.
The conveyor pattern worked for us when we made the steps explicit and wrote evidence for each step:
ts
const defaults = { mode: 'warm', flags: { audit: true } };
const raw: any = parseLooseJson(input);
const seeded: any = { ...defaults, ...raw, meta: { receivedAt: Date.now() } };
const asserted = seeded as typeof defaults & { id?: string; meta?: any };
ledger.write('steps', [
...ledger.read('steps'),
{ at: Date.now(), kind: 'seed', value: String(asserted.mode) },
{ at: Date.now(), kind: 'assert', value: String(Boolean(asserted.flags?.audit)) },
]);
If you don't record the steps, it's just "somewhere we asserted" and nobody can debug it.
Counterpoint: "any-first" can also become permission to never model reality.
I've seen conveyors turn into a pile of as casts where the UI renders nonsense and nobody notices until production.
Totally. The guardrail is the contract marker + versioning.
If the route publishes data-contract-version and the ledger shows assertion steps, you can at least tell when you're guessing vs when you're conforming.
Yeah, visibility is the difference between momentum and chaos.
A tiny trick for keeping generics from becoming narrative noise: keep them at container boundaries only.
If a component needs three generic params, it's usually a sign the surface isn't stable enough to describe plainly.
Unified TS/JS is fine, but I'd still keep one "tightening point" per route so the rest of the tree can be boring.
The conveyor metaphor helps because it suggests: widen once, tighten once, then transport in a box.