ReviewOS

also looking at this

stacks/bun-query-builder

refactor(types): stop publishing `any` across the query builder's surface

#1117
Merged chris wants to merge types/remove-any-from-public-surface into main
13 files +359 -110
9 days ago

An any in a published .d.ts is not a local shortcut - it is a hole in every consumer's typechecker. An app writing db.selectFrom('users') got no help from the compiler because the builder it received had been typed any three files upstream. That is how installed Stacks apps ended up seeing their own models as any.

The published surface goes 91 -> 18, and the two files an app touches on every query reach zero:

filebeforeafter
client.d.ts290
orm.d.ts40
dynamodb-client.d.ts180
db.d.ts117
everything else2911

What replaced them

  • AnyDatabaseSchema - 21 generic constraints read DB extends DatabaseSchema<any>, which says "a schema of anything" and dragged any into every signature mentioning one. A schema is tables, each with columns, a primary key and optional relations, so it now says that.
  • DriverConnection for the internal sql handle, and EmbeddedValue / asEmbeddedValue for the three shapes a tagged-template value can take (bind param, nested query, raw marker) - so the branch that fixed #1084 reads properties without an any.
  • toBindings - the single place user values become driver bindings. Unchecked on purpose (bun:sqlite is the validator and throws a clear "can't bind"), but named once instead of an any at six call sites.
  • DynamoDBValue / DynamoDBClientLike / DynamoDBFilterOperator / DynamoDBConsumedCapacity - DynamoDB's type system is small and closed, so the fluent surface can name it. The AWS SDK is not a dependency, so the client is structural.
  • Real overloads on the browser client's where/orWhere/andWhere. WhereOperator | any collapses to any and checked nothing; the overloads mean a three-arg call must pass an actual operator.
  • Inputs widened to unknown rather than narrowed - nothing breaks, everything is assignable to it.

The survivors are deliberate

Each carries its reason in the source: driver escape hatches in db.ts (narrowing forces a cast at every await sql.unsafe(...) - the same hole, moved somewhere less visible), DeepPartial's function-detection idiom, the faker proxy's dynamic members, and accessor/mutator parameters in schema.ts. That last one matters: a model's own get: { name: (value: string) => ... } is not assignable to (value: unknown) => ... under strictFunctionTypes, so narrowing there would reject every correctly written model. Their returns are unknown, so nothing downstream inherits the looseness.

Verification

  • bun run typecheck clean
  • 2000 pass / 0 fail, unchanged from before
  • bunx --bun pickier . 0 errors
  • A new test pins the count, so the next any needs a justification rather than a silent bump

13 changed files on the files tab, with 0 review threads.

Sign in to comment on this pull request.