feat(deps): bump zod 3→4 + @hookform/resolvers 3→5

Resolved 65 type errors across the codebase via these v4 migration
patterns:

- `ZodError.errors` renamed to `ZodError.issues` (4 call sites in auth
  routes + central error handler).
- `z.record(value)` now requires explicit key type: `z.record(z.string(),
  value)`. Updated 7 sites across templates / forms / saved-views /
  website-inquiries.
- `.refine(check, msgFn)` second-arg shape changed — now requires an
  `{ error: (issue) => ... }` object form. Updated
  `mergeFieldsSchema` in document-templates validator.
- `.transform(...).default(...)` chains: v4 enforces default value type
  matches transform OUTPUT. Reordered to `.default(...).transform(...)`
  in list-query / company-memberships handlers.
- `z.coerce.*()` INPUT type widened to `unknown` in v4. Service signatures
  using `z.input<typeof schema>` (kept for caller flexibility around
  defaults) now re-parse via `schema.parse(data)` to recover the
  post-coercion shape Drizzle needs. Done in berth-reservations service.
  Invoice service narrows `lineItems` locally with a typed cast since
  re-parsing would double-validate.
- `.optional().transform(...)` no longer propagates the optional marker
  through v4's new ZodPipe. Moved `.optional()` to the END of chain in
  `optionalDesiredDimSchema` (interests) and documents list query
  (folderId, signatureOnly).
- ZodIssue subtype shapes simplified: `received` removed from
  invalid_type, `type` renamed to `origin` on too_small. Test fixtures
  updated.
- @hookform/resolvers v5 splits Resolver into 3-generic form (Input,
  Context, Output). useForm calls in 6 forms (client, yacht, berth,
  interest, expense, invoices-new-page) now pass explicit generics:
  `useForm<z.input<typeof schema>, unknown, z.infer<typeof schema>>`.

Verified: tsc clean (0 errors), vitest 1293/1293 pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-12 18:29:03 +02:00
parent d3960af340
commit acf878f997
24 changed files with 131 additions and 90 deletions

View File

@@ -24,6 +24,7 @@ import { CurrencyInput } from '@/components/shared/currency-input';
import { CurrencySelect } from '@/components/shared/currency-select';
import { apiFetch } from '@/lib/api/client';
import { toastError } from '@/lib/api/toast-error';
import type { z } from 'zod';
import { updateBerthSchema, type UpdateBerthInput } from '@/lib/validators/berths';
import {
BERTH_AREAS,
@@ -120,7 +121,7 @@ export function BerthForm({ berth, open, onOpenChange }: BerthFormProps) {
setValue,
watch,
formState: { isSubmitting },
} = useForm<UpdateBerthInput>({
} = useForm<z.input<typeof updateBerthSchema>, unknown, UpdateBerthInput>({
resolver: zodResolver(updateBerthSchema),
defaultValues: {
area: berth.area ?? undefined,
@@ -403,7 +404,7 @@ export function BerthForm({ berth, open, onOpenChange }: BerthFormProps) {
<div className="space-y-2">
<Label>Price</Label>
<CurrencyInput
value={watch('price') ?? ''}
value={(watch('price') as number | null | undefined) ?? ''}
currency={watch('priceCurrency') ?? 'USD'}
onChange={(v) => setValue('price', v ?? undefined, { shouldDirty: true })}
/>

View File

@@ -25,6 +25,7 @@ import { TimezoneCombobox } from '@/components/shared/timezone-combobox';
import { PhoneInput } from '@/components/shared/phone-input';
import { DedupSuggestionPanel } from '@/components/clients/dedup-suggestion-panel';
import { apiFetch } from '@/lib/api/client';
import type { z } from 'zod';
import { createClientSchema, type CreateClientInput } from '@/lib/validators/clients';
import { SOURCES } from '@/lib/constants';
import type { CountryCode } from '@/lib/i18n/countries';
@@ -74,7 +75,7 @@ export function ClientForm({ open, onOpenChange, client, onUseExistingClient }:
setValue,
reset,
formState: { errors, isSubmitting },
} = useForm<CreateClientInput>({
} = useForm<z.input<typeof createClientSchema>, unknown, CreateClientInput>({
resolver: zodResolver(createClientSchema),
defaultValues: {
fullName: '',

View File

@@ -23,6 +23,7 @@ import { CurrencyInput } from '@/components/shared/currency-input';
import { CurrencySelect } from '@/components/shared/currency-select';
import { TripLabelCombobox } from '@/components/expenses/trip-label-combobox';
import { apiFetch } from '@/lib/api/client';
import type { z } from 'zod';
import { createExpenseSchema, type CreateExpenseInput } from '@/lib/validators/expenses';
import { EXPENSE_CATEGORIES, PAYMENT_METHODS } from '@/lib/constants';
import type { ExpenseRow } from './expense-columns';
@@ -55,7 +56,7 @@ export function ExpenseFormDialog({ open, onOpenChange, expense }: ExpenseFormDi
reset,
watch,
formState: { errors, isSubmitting },
} = useForm<CreateExpenseInput>({
} = useForm<z.input<typeof createExpenseSchema>, unknown, CreateExpenseInput>({
resolver: zodResolver(createExpenseSchema),
defaultValues: {
currency: 'USD',
@@ -211,7 +212,7 @@ export function ExpenseFormDialog({ open, onOpenChange, expense }: ExpenseFormDi
<Label htmlFor="amount">Amount *</Label>
<CurrencyInput
id="amount"
value={watch('amount') ?? ''}
value={(watch('amount') as number | null | undefined) ?? ''}
currency={watch('currency') ?? 'USD'}
onChange={(v) =>
setValue('amount', v ?? Number.NaN, { shouldDirty: true, shouldValidate: true })

View File

@@ -45,6 +45,7 @@ import { YachtForm } from '@/components/yachts/yacht-form';
import { YachtPicker } from '@/components/yachts/yacht-picker';
import { apiFetch } from '@/lib/api/client';
import { useEntityOptions } from '@/hooks/use-entity-options';
import type { z } from 'zod';
import { createInterestSchema, type CreateInterestInput } from '@/lib/validators/interests';
import { PIPELINE_STAGES, STAGE_LABELS, LEAD_CATEGORIES, SOURCES } from '@/lib/constants';
import { cn } from '@/lib/utils';
@@ -96,7 +97,7 @@ export function InterestForm({ open, onOpenChange, defaultClientId, interest }:
setValue,
reset,
formState: { errors, isSubmitting, isDirty },
} = useForm<CreateInterestInput>({
} = useForm<z.input<typeof createInterestSchema>, unknown, CreateInterestInput>({
resolver: zodResolver(createInterestSchema),
defaultValues: {
clientId: '',

View File

@@ -23,6 +23,7 @@ import { CountryCombobox } from '@/components/shared/country-combobox';
import { OwnerPicker, type OwnerRef } from '@/components/shared/owner-picker';
import { TagPicker } from '@/components/shared/tag-picker';
import { apiFetch } from '@/lib/api/client';
import type { z } from 'zod';
import { createYachtSchema, type CreateYachtInput } from '@/lib/validators/yachts';
interface YachtFormProps {
@@ -73,7 +74,7 @@ export function YachtForm({ open, onOpenChange, yacht, initialOwner }: YachtForm
setValue,
reset,
formState: { errors, isSubmitting },
} = useForm<CreateYachtInput>({
} = useForm<z.input<typeof createYachtSchema>, unknown, CreateYachtInput>({
resolver: zodResolver(createYachtSchema),
defaultValues: {
name: '',