0 %

Building a TypeScript-Powered Dynamic Form Engine with Recursive Schema Validation

TypeScript development

Dynamic forms that adapt to user input and validate complex nested data structures are a common requirement in enterprise applications. Building a type-safe form engine that handles recursive schemas while maintaining excellent developer experience requires careful architectural decisions.

This article explores how to leverage TypeScript's advanced type system to build a form engine that provides compile-time safety, runtime validation, and seamless integration with popular UI frameworks.

Table of contents:

Schema Design Principles

A well-designed form schema should be declarative, composable, and self-documenting. Using TypeScript's discriminated unions, we can create field types that carry their validation rules and UI hints as part of their type definition.

The schema should support primitive fields, nested objects, arrays of items, and conditional fields that appear based on other field values—all while maintaining full type inference.

TypeScript's type system is Turing complete. If you can describe it, you can type it—and your form engine should prove that.

Matt Pocock, TypeScript Educator

Type-Safe Field Definitions

Key patterns for building type-safe field definitions:

  1. Discriminated Unions
    • Use a 'type' property to distinguish field kinds.
    • TypeScript narrows types automatically in switch statements.
  2. Generic Constraints
    • Constrain field value types based on field type.
    • Ensure validators receive correctly typed values.
  3. Mapped Types
    • Generate form state types from schema definitions.
    • Automatically infer required vs optional fields.
Code editor
Type system diagram

Recursive Validation

Handling nested schemas requires recursive type definitions and validation logic:

Recursive Type Definitions

Use TypeScript's recursive type aliases to define schemas that can contain themselves. The 'infer' keyword helps extract nested types for validation functions.

Depth-First Validation

Validate nested objects by recursively calling the validator on child schemas. Collect errors at each level and return a structured error object that mirrors the form structure.

Array Field Handling

For array fields, validate each item against the item schema and aggregate errors with their indices. Support min/max length constraints at the array level.

Programming code
Validation flow
Programming code
Error structure

Conditional Field Logic

When Conditions. Define conditions that reference other field values using path expressions. The form engine evaluates these conditions on every change to show/hide fields dynamically.

Dependent Validation. Fields that only appear conditionally should only validate when visible. Track field visibility state and skip validation for hidden fields.

Type Narrowing. Use conditional types to narrow the form output type based on which conditional branches are active, ensuring type safety even with dynamic fields.

Framework Integration

Integrating the form engine with popular frameworks:

  • React Hook Form: Create a useSchemaForm hook that generates register functions and validation rules from your schema;
  • Vue Composition API: Build reactive form state using ref and computed properties that sync with schema validation;
  • Svelte Stores: Leverage Svelte's reactive stores to create a lightweight form state management solution.

Performance Optimization

Strategies for keeping form validation fast:

  • Memoization: Cache validation results for unchanged fields to avoid redundant computation;
  • Debounced Validation: Delay validation until the user stops typing to reduce CPU usage during rapid input;
  • Lazy Schema Compilation: Pre-compile validation functions from schemas at build time using code generation.

Icon Let's talk about your project!

Image Image