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
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'
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