Skip to content

[no-ticket] Catch nullable values laundered through TypeScript assertions - #49

Open
bockets wants to merge 1 commit into
mainfrom
no-ticket-nullability-boundary-rule
Open

[no-ticket] Catch nullable values laundered through TypeScript assertions#49
bockets wants to merge 1 commit into
mainfrom
no-ticket-nullability-boundary-rule

Conversation

@bockets

@bockets bockets commented Sep 5, 2026

Copy link
Copy Markdown
Member

BiggiePockets reviewed and approved PRs #30877, #30879, #30880, #30881, and
#30883 in BiggerPockets/biggerpockets (a React port of the /notifications
page). The page then crashed on first load in a review app:

TypeError: Cannot read properties of null (reading 'charAt')

The root cause: colleague_requests.status and referenceships.status are
nullable columns in db/schema.rb (no null: false), and real rows carry
NULL. app/serializers/api/v3/notification_serializer.rb passed the column
straight through (status: parent.status). frontend/api/notifications/index.ts
declared status: string — not string | null — and laundered the null
through an unchecked assertion, status: raw.status as string. A component
then called status.charAt(0), threw, and the throw hit React Router's error
boundary, blanking the entire page rather than just the one notification
card. Test factories always set a status, so no spec caught it, and the
review missed it too.

This adds prompts/_shared/nullability-boundary-rules.md and wires it into
both review stages (codex-first-pass.md, claude-synthesize.md,
claude-synthesize-thesis-first.md):

  • Check the actual nullability of a value at its source (db/schema.rb) when
    it crosses a DB column → serializer → TypeScript API type → component
    boundary, rather than trusting a declared type further down the chain.
  • Treat an unchecked as assertion that narrows a nullable value to a
    non-null TypeScript type as a finding unless it's justified against the
    real schema/serializer.
  • Weigh blast radius: a throw inside a component subtree under an error
    boundary (React Router's route boundary, or an explicit ErrorBoundary)
    takes down everything the boundary covers, not just the failing element.

Also adds the file to the prompts/ tree listing in README.md.

Codex/Claude approved biggerpockets PRs #30877/#30879/#30880/#30881/#30883 (a
React port of /notifications) that crashed on first load in a review app:
TypeError: Cannot read properties of null (reading 'charAt'). The
colleague_requests.status and referenceships.status columns are nullable in
db/schema.rb, the serializer passed the column straight through, and the
TypeScript layer declared status: string and laundered the null through an
unchecked `as raw.status as string` assertion, which the component then
called .charAt(0) on. The throw hit React Router's error boundary and blanked
the whole page. Test factories always set a status, so no spec caught it.

Adds prompts/_shared/nullability-boundary-rules.md, wired into both review
stages, to check nullability at the schema/serializer source rather than
trusting a declared TypeScript type, and to weigh blast radius when a new
throw sits inside a subtree covered by an error boundary.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant