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
| Utility | Input | Result |
|---|---|---|
InferFieldValue<T> | One bd.* field | That field's value, including nullability |
InferFieldsValues<T> | A field declaration map | One object with required and optional keys |
InferCollectionValues<T> | One collection() result | The collection's value object |
InferFormValues<T> | A field map, collection, or collection map | Values for the supplied form boundary |
InferSchemaValues<T> | A named map of collections | A 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.