Asked to find where the codebase casts instead of using its generated types. The
gates say the obvious places are clean — validate:boundary-casts is at 0,
validate:untyped-db-reads reports none, and every lakehouse read is already
parsed against LAKEHOUSE_ROW_SCHEMAS. The casts are somewhere else, and they
have one cause.
The measurement
330 as unknown as in src/ + workers/, and they are not scattered:
119 as unknown as Parameters<typeof f>[n]
77 as unknown as Record<…>
34 as unknown as Array<…>
12 as unknown as Row (the documented boundary cast; not a violation)
11 as unknown as Env
Concentrated in six files: entities.ts 62, api.ts 37, graphql.ts 34,
mcp-server.ts 31, analytics.ts 30, data-api.ts 24.
Why the biggest category exists
Every one of the 119 looks like this:
await loadSubnetWeightsColdTier(
env as unknown as Parameters<typeof loadSubnetWeightsColdTier>[0],
…
)
and the loader declares:
export async function loadSubnetWeightsColdTier(env: Parameters<R2SqlReader>[0], …)
// ^ resolves to `Env | null | undefined`
src/r2-sql.ts imports no Env. It uses the ambient global one, which is
generated per Worker:
workers/data-api.worker-configuration.d.ts 16 bindings
workers/registry-sync-api.worker-configuration.d.ts
workers/wss-lb.worker-configuration.d.ts 7 bindings
Three Workers, three different Env shapes, one shared src/. So shared code is
typed against whichever ambient Env wins, every Worker whose bindings differ
fails to satisfy it structurally, and the call site forces it. There is at least
one triple cast from the same cause —
readStore(env, …) as never as unknown as Parameters<…> in entities.ts:7787.
What the cast actually costs
It is not cosmetic. as unknown as is the one form that survives BOTH sides
changing: if a loader starts reading a binding the calling Worker does not have,
nothing fails to compile and the read gets undefined at runtime. That is the
#436 class — a knob that looks wired and is not — reachable at 119 sites.
The shape of the fix
The same one #11207 used for StatementClientLike: shared code should declare
the MINIMAL binding surface it needs, not the ambient Env. r2SqlQuery reads
one binding. A loader that says
env: { R2_SQL_TOKEN?: string; … } | null | undefined
is satisfied structurally by every Worker's Env, and the cast at each call site
disappears rather than being suppressed.
Worth doing per-family rather than in one change — the Parameters<R2SqlReader>[0]
indirection alone appears in 13 loaders, and those share one contract.
Not in scope
The 77 Record<…> and 34 Array<…> casts are a separate question and some are
legitimate (reading an untyped JSON body). They should be counted again once the
Parameters family is gone, because that is where the noise is now.
Found while surveying for #11207-style drift; related: #11194 (cast vs parse at
real boundaries), #11182 S5.
Asked to find where the codebase casts instead of using its generated types. The
gates say the obvious places are clean —
validate:boundary-castsis at 0,validate:untyped-db-readsreports none, and every lakehouse read is alreadyparsed against
LAKEHOUSE_ROW_SCHEMAS. The casts are somewhere else, and theyhave one cause.
The measurement
330
as unknown asinsrc/+workers/, and they are not scattered:Concentrated in six files:
entities.ts62,api.ts37,graphql.ts34,mcp-server.ts31,analytics.ts30,data-api.ts24.Why the biggest category exists
Every one of the 119 looks like this:
and the loader declares:
src/r2-sql.tsimports noEnv. It uses the ambient global one, which isgenerated per Worker:
Three Workers, three different
Envshapes, one sharedsrc/. So shared code istyped against whichever ambient
Envwins, every Worker whose bindings differfails to satisfy it structurally, and the call site forces it. There is at least
one triple cast from the same cause —
readStore(env, …) as never as unknown as Parameters<…>inentities.ts:7787.What the cast actually costs
It is not cosmetic.
as unknown asis the one form that survives BOTH sideschanging: if a loader starts reading a binding the calling Worker does not have,
nothing fails to compile and the read gets
undefinedat runtime. That is the#436 class — a knob that looks wired and is not — reachable at 119 sites.
The shape of the fix
The same one #11207 used for
StatementClientLike: shared code should declarethe MINIMAL binding surface it needs, not the ambient
Env.r2SqlQueryreadsone binding. A loader that says
is satisfied structurally by every Worker's
Env, and the cast at each call sitedisappears rather than being suppressed.
Worth doing per-family rather than in one change — the
Parameters<R2SqlReader>[0]indirection alone appears in 13 loaders, and those share one contract.
Not in scope
The 77
Record<…>and 34Array<…>casts are a separate question and some arelegitimate (reading an untyped JSON body). They should be counted again once the
Parametersfamily is gone, because that is where the noise is now.Found while surveying for #11207-style drift; related: #11194 (cast vs parse at
real boundaries), #11182 S5.