Skip to main content
New to FormEngine? It’s a React library that renders forms from JSON schemas — free, MIT-licensed. Start here →
FormEngine is written in TypeScript and ships with full type definitions. This guide covers every pattern for strongly-typed forms: typing the JSON schema, inferring form data types, writing generic components, and end-to-end type safety from schema to submit handler.

Install types

Types ship with every @react-form-builder/* package — no @types/ install needed.
npm install @react-form-builder/core @react-form-builder/components-rsuite

Typing form data

FormEngine’s IFormData interface describes the shape of a form’s runtime state. Use it when reading data in callbacks, actions, or refs.
import { IFormData, IFormViewer } from '@react-form-builder/core'

interface UserFormData {
  email: string
  age: number
  role: 'admin' | 'user'
}

const handleChange = (formData: IFormData) => {
  const data = formData.data as UserFormData  // cast to your known shape
  console.log(data.email)                     // ✓ typed
}

Typed initialData

initialData accepts any serializable object — type it with your form shape for full autocomplete and compile-time checks.
const initialData: UserFormData = {
  email: 'alice@example.com',
  age: 30,
  role: 'user',
}

<FormViewer<UserFormData>
  view={view}
  getForm={getForm}
  initialData={initialData}
  onFormDataChange={(fd) => {
    const data = fd.data as UserFormData
    // data.email ✓
  }}
/>

Typed custom actions

Actions receive an ActionEventArgs object. Cast e.data to your form shape inside the action body:
import { ActionDefinition, ActionEventArgs } from '@react-form-builder/core'

interface CheckoutData {
  amount: number
  currency: 'USD' | 'EUR'
  cardToken: string
}

const customActions = {
  processPayment: ActionDefinition.functionalAction(async (e: ActionEventArgs) => {
    const data = e.data as CheckoutData
    await charge(data.amount, data.currency, data.cardToken)
  }),
}

Typed viewerRef

Use IFormViewer for the ref type, then cast formData.data when reading:
const viewerRef = useRef<IFormViewer>(null)

const handleSubmit = async () => {
  const viewer = viewerRef.current
  if (!viewer) return
  const data = viewer.formData.data as UserFormData
  await submit(data)
}

Typed custom components

When you register a custom React component with FormEngine, type its props with the fields you expose:
import { define, boolean, string } from '@react-form-builder/core'

interface MyInputProps {
  label: string
  required?: boolean
  placeholder?: string
  value?: string
  onChange?: (value: string) => void
}

const MyInput = ({ label, required, placeholder, value, onChange }: MyInputProps) => (
  <label>
    {label}{required ? ' *' : ''}
    <input
      value={value ?? ''}
      placeholder={placeholder}
      onChange={(e) => onChange?.(e.target.value)}
    />
  </label>
)

export const myInput = define(MyInput, 'MyInput')
  .props({
    label: string,
    required: boolean,
    placeholder: string,
  })
  .build()

Inferring types from Zod validation

FormEngine validation is Zod-powered. Share one Zod schema between runtime validation and TypeScript types for a single source of truth:
import { z } from 'zod'

const signupSchema = z.object({
  email: z.string().email(),
  password: z.string().min(8),
  age: z.number().int().min(18),
})

type SignupData = z.infer<typeof signupSchema>

const handleSubmit = (data: SignupData) => {
  const parsed = signupSchema.parse(data)  // runtime check
  postToApi(parsed)
}
See the Zod validation tutorial for wiring this schema into FormEngine’s validation pipeline.

Strict JSON schemas with as const

If you define your form JSON inline, use as const to preserve literal types for component keys:
const form = {
  form: {
    key: 'Screen',
    type: 'Screen',
    children: [
      { key: 'email', type: 'RsInput' },
      { key: 'age', type: 'RsNumberFormat' },
    ],
  },
} as const

type FormKeys = typeof form.form.children[number]['key']
// FormKeys = 'email' | 'age'

Generic form components

Build reusable form wrappers that are generic over data shape:
interface TypedFormProps<T> {
  schema: string
  initialData: T
  onSubmit: (data: T) => Promise<void>
}

function TypedForm<T extends Record<string, unknown>>({
  schema,
  initialData,
  onSubmit,
}: TypedFormProps<T>) {
  const viewerRef = useRef<IFormViewer>(null)

  const handleSubmit = async () => {
    const data = viewerRef.current?.formData.data as T
    await onSubmit(data)
  }

  return (
    <>
      <FormViewer
        view={view}
        getForm={() => schema}
        initialData={initialData}
        viewerRef={viewerRef}
      />
      <button onClick={handleSubmit}>Submit</button>
    </>
  )
}

// Usage with full type inference
<TypedForm<UserFormData>
  schema={userSchema}
  initialData={{ email: '', age: 0, role: 'user' }}
  onSubmit={async (data) => {
    // data is UserFormData — .email, .age, .role all typed
  }}
/>

Event handler types

Built-in events have typed signatures. Import them from @react-form-builder/core:
import type {
  FormViewerProps,
  IFormData,
  ValidationResult,
} from '@react-form-builder/core'

const onFormDataChange: FormViewerProps['onFormDataChange'] = (formData) => {
  // formData: IFormData
}

Common TypeScript gotchas

  • e.data is unknown in actions. Cast to your form shape — FormEngine can’t infer it from the schema at runtime.
  • formData.errors vs validation result types differ. errors is the currently-shown error map; getValidationResult() returns every error regardless of display state.
  • Custom component props require type annotations for define() to compile. Use as any sparingly — prefer explicit interfaces.
  • Don’t use tsconfig strict: false to paper over type issues. Keep strict mode on; cast at boundaries.
Last modified on April 16, 2026