Skip to content

TypeScript ​

A definition is data, so TypeScript can't see its fields. Below you will learn the two ways to bring types back: typing a definition you write in code, and typing the form a definition builds.

Writing definitions in code ​

When you write a definition in TypeScript, defineForm() checks it against the builder's registries, your own types and rules included. It returns the definition unchanged:

ts
import {
  createSchemaBuilder,
  defineFieldType,
} from '@whatsaaaa/form-builder-schema'
import * as v from 'valibot'

const money = defineFieldType({
  kind: 'number',
  props: v.object({ currency: v.picklist(['EUR', 'USD']) }),
  schema: () => v.number(),
})

const { defineForm, build } = createSchemaBuilder({ fieldTypes: { money } })

const form = defineForm({
  version: '1',
  fields: [
    {
      name: 'email',
      type: 'email',
      rules: [{ rule: 'maxLength', value: 120 }],
    },
    { name: 'price', type: 'money', currency: 'EUR' }, // currency is typed by money's props
  ],
})

const schema = build(form)

These are compile errors:

ts
defineForm({
  version: '1',
  fields: [
    // @ts-expect-error: 'emial' is no registered type
    { name: 'email', type: 'emial' },
  ],
})
defineForm({
  version: '1',
  fields: [
    {
      name: 'email',
      type: 'email',
      // @ts-expect-error: the value of maxLength is a number
      rules: [{ rule: 'maxLength', value: '120' }],
    },
  ],
})
  • Checked: unknown types, rules and form rules, keys that are neither the keys every field takes nor props of its type, and props and params of the wrong type. Child fields of groups and lists are checked the same way, and so are the layout's nodes, with display elements typed by their props, and affects with the affects package.
  • Not checked: whether a rule fits the field's kind, names, defaults, the paths of form rules and of the layout, and spans. validate() checks those.
  • The builder knows your types and rules by name when you pass them in the options, as above. Options typed as SchemaBuilderOptions have no known names, so defineForm then takes any type.

Typing the form ​

The output of a built schema is an object of unknown values, and Formisch's functions take only top-level paths on it. When you know the shape, give build() your input type, and your output type when it differs:

ts
interface SignUpInput {
  email: string
  address: { city?: string }
}

const schema = builder.build<SignUpInput>(json)

const output = v.parse(schema, { email: 'jane@example.com', address: {} })
const city = output.address.city // string | undefined

The adapters' useJsonForm takes the same types, so Formisch checks paths and values as in any form:

ts
interface CheckoutInput {
  name: string
  address: { city: string }
  lines: { product: string; quantity: number }[]
}

const jsonForm = useJsonForm<CheckoutInput>(definition, { components })

getInput(jsonForm.form, { path: ['address', 'city'] }) // string | undefined
useFieldArray(jsonForm.form, { path: ['lines'] })
ts
interface CheckoutInput {
  name: string
  address: { city: string }
  lines: { product: string; quantity: number }[]
}

const jsonForm = useJsonForm<CheckoutInput>(definition, { components })

getInput(jsonForm.form, { path: ['address', 'city'] }) // string | undefined
useFieldArray(jsonForm.form, { path: ['lines'] })
  • It is the same schema, typed as TypedBuiltSchema. An interface works as the type, which Formisch's own constraint wouldn't take as it is.
  • The types are a promise you make. Nothing checks them against the definition, so keep the two in step.
  • With conditions, a field that can hide submits nothing while it is hidden, even a required one. Make it optional in the output type.
  • Paths that come from the definition, like a FieldNode's, still need as never for Formisch, since TypeScript can't know them.

Why the output isn't inferred ​

A field type's schema() returns any Valibot schema, so a definition doesn't say what type each value has, and v.InferOutput of a built schema is an object of unknown values. Inferring it would need every field type to declare its output type. Until then, give build() or useJsonForm your types.