types: clear remaining tsc errors across 41 files (76→14, behavior-preserving)

Per-file type-only fixes surfaced after the Supabase client was correctly
typed. All fixes preserve runtime behavior; no any/ts-ignore/ts-expect-error.

- lib/supabase/client.ts: annotate the browser client as SupabaseClient<Database>
  (same ssr/js drift as server.ts; fixes it centrally for all callers).
- Supabase embedded-select joins whose FK metadata is missing from the
  generated types are bridged with `as unknown as <target>` + a TODO to remove
  after regenerating types/database.ts.
- setState optimistic-update variance and raw <select> enum values cast to the
  precise element/union type; .insert payload fields (Json, enums) given the
  precise column type via satisfies/field casts.
- Added narrowing guards where a value was legitimately nullable (comms channel,
  public form page) without changing the intended path.

The 14 remaining errors are confined to the four files fixed in Phase B (real
bugs + the tautological actions.test rewrite). 639 tests still green.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
This commit is contained in:
Moheeb Zara
2026-08-01 18:56:11 -07:00
co-authored by Claude Opus 4.8
parent 1766780bca
commit b1edb5c460
41 changed files with 148 additions and 59 deletions
+24 -3
View File
@@ -432,7 +432,26 @@ export async function listUpcomingSessions() {
.order('starts_at', { ascending: true })
if (error) return { error: error.message }
const rows = (sessions ?? []).filter(
// TODO(types): remove after regenerating types/database.ts (missing FK relationship metadata)
const rows = ((sessions ?? []) as unknown as Array<{
id: string
class_id: string
starts_at: string
ends_at: string | null
location: string | null
capacity: number | null
status: string
notes: string | null
classes: {
title: string
description: string | null
payment_link: string | null
capacity: number | null
is_active: boolean
grants_certification_id: string | null
required_form_id: string | null
} | null
}>).filter(
(s: { classes?: { is_active?: boolean } | null }) => s.classes?.is_active !== false,
)
const ids = rows.map((s: { id: string }) => s.id)
@@ -536,7 +555,8 @@ export async function signUpForClass(input: unknown) {
.eq('space_id', member.space_id)
.maybeSingle()
if (!session) return { error: 'Session not found' }
const cls = (session as {
// TODO(types): remove after regenerating types/database.ts (missing FK relationship metadata)
const cls = (session as unknown as {
classes?: { title: string | null; capacity: number | null; is_active: boolean; required_form_id: string | null } | null
}).classes
if (cls && cls.is_active === false) return { error: 'This class is no longer available.' }
@@ -827,7 +847,8 @@ export async function completeSession(input: unknown) {
.eq('space_id', member.space_id)
if (upErr) return { error: upErr.message }
const cls = (session as { classes?: { title: string; grants_certification_id: string | null } | null }).classes
// TODO(types): remove after regenerating types/database.ts (missing FK relationship metadata)
const cls = (session as unknown as { classes?: { title: string; grants_certification_id: string | null } | null }).classes
let issued = 0
let certSkipped = false
if (cls?.grants_certification_id) {
+2 -1
View File
@@ -619,7 +619,8 @@ export async function listDoorCards(input: unknown) {
return {
data: (cards ?? []).map(c => {
const sm = c.space_members as { display_name: string | null } | { display_name: string | null }[] | null
// TODO(types): remove after regenerating types/database.ts (missing FK relationship metadata)
const sm = c.space_members as unknown as { display_name: string | null } | { display_name: string | null }[] | null
const name = Array.isArray(sm) ? sm[0]?.display_name : sm?.display_name
return {
cardId: c.id as string,
+2 -1
View File
@@ -41,6 +41,7 @@ import {
getSpaceName,
buildManageUrl,
} from '@/lib/notifications/enqueue'
import type { Json } from '@/types/database'
// Associate prior unlinked submissions in a space with a member by email
// (case-insensitive, ILIKE-escaped so `_`/`%` in an address are literal).
@@ -530,7 +531,7 @@ export async function submitForm(input: unknown) {
space_id: form.space_id,
member_id: linkedMemberId,
submitter_email: submitterEmail,
answers: answers.value,
answers: answers.value as Json,
form_snapshot: form.schema,
legal_text_snapshot: form.legal_text ?? null,
form_version: form.version,
+2 -1
View File
@@ -5,6 +5,7 @@ import { createClient } from '@/lib/supabase/server'
import { requireMemberWithRole } from '@/lib/auth-helpers'
import { ADMIN_ROLES } from '@/lib/permissions'
import { importMembersSchema } from '@/lib/validations'
import type { TablesInsert } from '@/types/database'
export async function importMembers(rows: unknown) {
if (!Array.isArray(rows) || rows.length === 0) {
@@ -19,7 +20,7 @@ export async function importMembers(rows: unknown) {
// Validate per row so one bad row does not reject the whole file; the
// schema lowercases email and normalizes dates via flexibleDateTime.
const rowSchema = importMembersSchema.element
const valid: Array<Record<string, unknown>> = []
const valid: Array<TablesInsert<'space_members'>> = []
let skipped = 0
for (const raw of rows) {
const r = rowSchema.safeParse(raw)
+2 -1
View File
@@ -3,6 +3,7 @@
import { revalidatePath } from 'next/cache'
import { createClient } from '@/lib/supabase/server'
import { createAdminClient } from '@/lib/supabase/admin'
import type { Json } from '@/types/database'
import { requireMember, requireMemberWithRole, parseInput } from '@/lib/auth-helpers'
import {
createOnboardingStepSchema,
@@ -45,7 +46,7 @@ export async function createOnboardingStep(input: {
step_type: v.data.step_type,
title: v.data.title,
body: v.data.body ?? null,
config: v.data.config ?? {},
config: (v.data.config ?? {}) as Json,
is_enabled: v.data.is_enabled ?? true,
is_required: v.data.is_required ?? false,
is_system: false,
+2 -2
View File
@@ -15,7 +15,7 @@ import {
linkPaymentSchema,
importPaymentsCsvSchema,
} from '@/lib/validations'
import type { Enums } from '@/types/database'
import type { Enums, TablesInsert } from '@/types/database'
type PaymentPlatform = Enums<'payment_platform'>
@@ -131,7 +131,7 @@ export async function importPaymentsCsv(rows: unknown) {
// transaction_date. Invalid rows are skipped and counted, not silently
// dropped without feedback.
const rowSchema = importPaymentsCsvSchema.element
const inserts: Array<Record<string, unknown>> = []
const inserts: Array<TablesInsert<'payments'>> = []
let skipped = 0
for (const raw of rows) {
const r = rowSchema.safeParse(raw)
+2 -1
View File
@@ -2,6 +2,7 @@
import { revalidatePath } from 'next/cache'
import { createClient } from '@/lib/supabase/server'
import type { TablesInsert } from '@/types/database'
import { requireMember, requireMemberWithRole, parseInput } from '@/lib/auth-helpers'
import { ADMIN_ROLES } from '@/lib/permissions'
import { createSecretSchema, updateSecretSchema, uuidSchema } from '@/lib/validations'
@@ -45,7 +46,7 @@ export async function createSecret(formData: {
const { data, error } = await supabase
.from('secrets')
.insert(row)
.insert(row as TablesInsert<'secrets'>)
.select('id, title, label, area, description, icon, encryption_version, created_at, updated_at, space_id, created_by')
.single()
+2 -1
View File
@@ -257,7 +257,8 @@ export async function listMemberBilling() {
if (error) return { error: error.message }
return {
data: (data ?? []).map(r => {
const sm = r.space_members as { display_name: string | null; email: string | null; tier: string | null } | { display_name: string | null; email: string | null; tier: string | null }[] | null
// TODO(types): remove after regenerating types/database.ts (missing FK relationship metadata)
const sm = r.space_members as unknown as { display_name: string | null; email: string | null; tier: string | null } | { display_name: string | null; email: string | null; tier: string | null }[] | null
const mem = Array.isArray(sm) ? sm[0] : sm
return {
memberId: r.member_id as string,