Skip to content

types: 119 casts exist because shared src/ is typed against ONE Worker's ambient Env #11339

Description

@JSONbored

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.

Metadata

Metadata

Assignees

Labels

No labels
No labels

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions