Inferred Types and Custom Forms

A BusinessDash collection declaration can be the compile-time source of truth even when BusinessDash is not rendering the form. Infer the submitted value shape and use it in a custom React form, a route handler, a content component, or any other code-owned UI.

import {
  Enums,
  bd,
  collection,
  type InferCollectionValues,
  type InferSchemaValues,
} from '@businessdash/sdk'

const ContactStatus = Enums(['new', 'qualified', 'closed'] as const)

export const contacts = collection('contacts', {
  fields: {
    fullName: bd.text().required(),
    email: bd.email().optional(),
    status: bd.enum(ContactStatus).required(),
    profile: bd.object({
      company: bd.text().required(),
      title: bd.text().optional(),
    }).required(),
  },
})

export type ContactValues = InferCollectionValues<typeof contacts>

The inferred ContactValues is equivalent to:

type ContactValues = {
  fullName: string
  email?: string | null | undefined
  status: 'new' | 'qualified' | 'closed'
  profile: {
    company: string
    title?: string | null | undefined
  }
}

Inference preserves:

  • .required() versus optional fields
  • .nullable(false) versus nullable values
  • exact Enums() and literal-array choices
  • nested bd.object() properties
  • number, boolean, array, relation, file, JSON, and measurement value shapes

Choose the inference boundary

UtilityInputResult
InferFieldValue<T>One bd.* fieldThat field's value, including nullability
InferFieldsValues<T>A field declaration mapOne object with required and optional keys
InferCollectionValues<T>One collection() resultThe collection's value object
InferFormValues<T>A field map, collection, or collection mapValues for the supplied form boundary
InferSchemaValues<T>A named map of collectionsA keyed value object for all collections

For a form that edits only part of a collection, use normal TypeScript:

type ContactIdentityValues = Pick<ContactValues, 'fullName' | 'email'>

For a custom form spanning collections:

const companies = collection('companies', {
  fields: {
    name: bd.text().required(),
  },
})

const onboardingCollections = { contacts, companies } as const

type OnboardingValues =
  InferSchemaValues<typeof onboardingCollections>

// {
//   contacts: ContactValues
//   companies: CompanyValues
// }

Pair the inferred type with Zod

The inferred type is a compile-time contract. Zod is a runtime validator. Use the BusinessDash type to ensure the hand-authored Zod schema stays aligned:

import { z } from 'zod'
import type { ContactValues } from './bd.data-model.config'

export const contactZodSchema: z.ZodType<ContactValues> = z.object({
  fullName: z.string().min(1),
  email: z.string().email().nullable().optional(),
  status: z.enum(['new', 'qualified', 'closed']),
  profile: z.object({
    company: z.string().min(1),
    title: z.string().nullable().optional(),
  }),
})

This follows the same useful end state as ZeraForm's generated Zod output: the runtime schema and an exported inferred value type agree. BusinessDash starts from the TypeScript bd declaration and infers its value type directly, so a code-generation step is not required.

TypeScript types disappear at runtime. InferCollectionValues does not validate an HTTP request or a visitor's input. Keep Zod, BusinessDash's field validators, and server validation at their appropriate runtime boundaries.

React Hook Form and shadcn/ui

React Hook Form accepts the inferred collection type as its form values. shadcn/ui form components use the same React Hook Form instance:

import { zodResolver } from '@hookform/resolvers/zod'
import { useForm } from 'react-hook-form'
import type { ContactValues } from './bd.data-model.config'
import { contactZodSchema } from './contact-zod'

export function ContactForm() {
  const form = useForm<ContactValues>({
    resolver: zodResolver(contactZodSchema),
    defaultValues: {
      fullName: '',
      status: 'new',
      profile: { company: '' },
    },
  })

  return (
    <form onSubmit={form.handleSubmit(saveContact)}>
      {/* shadcn/ui FormField components receive form.control */}
    </form>
  )
}

async function saveContact(values: ContactValues) {
  // values has the same compile-time shape as the BusinessDash collection.
}

Native number and date inputs may begin as strings in the browser. Coerce or transform those inputs in the form adapter or Zod schema before treating them as the inferred stored value type.

Type custom content too

The same type is useful when a component renders a manually fetched record rather than a BusinessDash-provided form:

export function ContactCard({ contact }: { contact: ContactValues }) {
  return (
    <article>
      <h2>{contact.fullName}</h2>
      <p>{contact.profile.company}</p>
    </article>
  )
}

The declaration now anchors the database field definitions, custom form values, runtime-validation schema, submit handler, and record-rendering props without forcing the UI to render a BusinessDash <Form formId="…" />.

For the runtime-validation boundaries, continue to Validation and Form State. For field syntax, use Schema Declarations.