Type-Safe Form Builder in TypeScript
Modern web applications heavily rely on forms for user input. Ensuring the type safety and structure of form data is crucial for preventing runtime errors and improving developer experience. This challenge asks you to build a foundational type-safe form builder in TypeScript.
Problem Description
Your goal is to create a TypeScript-based library that allows developers to define form structures with associated types. This library should enable the creation of forms where the shape of the form data is strongly typed, ensuring that when you access form values, TypeScript can verify their types at compile time.
Key Requirements:
- Form Definition: You need a way to define a form's structure. This structure should map field names to their respective data types.
- Type Inference: The library should infer the overall type of the form's data based on its definition.
- Field Access: Provide a mechanism to access individual form fields and their values, maintaining type safety.
- Form State Management (Basic): Implement a basic way to store and update form values, ensuring that updates adhere to the defined field types.
- Error Handling (Basic): A simple way to associate validation errors with specific fields, where errors also have a defined type (e.g.,
stringornull).
Expected Behavior:
When a form is defined, its data structure should be reflected in a TypeScript interface or type. Accessing a field should return a value strongly typed according to the definition. Updating a field should only accept values of the correct type.
Edge Cases to Consider:
- Forms with no fields.
- Fields with different primitive types (string, number, boolean).
- Fields that can be
nullorundefined.
Examples
Example 1: Basic String and Number Fields
Let's assume you have a FormBuilder class.
// Conceptual API
type UserFormDefinition = {
username: string;
age: number;
};
const userForm = new FormBuilder<UserFormDefinition>().addField('username', '').addField('age', 0);
// Accessing values
const username: string = userForm.getValue('username'); // Should be type string
const age: number = userForm.getValue('age'); // Should be type number
// Updating values
userForm.setValue('username', 'Alice');
userForm.setValue('age', 30);
// TypeScript should prevent:
// userForm.setValue('age', 'thirty'); // Error: Type 'string' is not assignable to type 'number'.
Output (Conceptual):
The userForm object would internally represent the form structure and its current values. The getValue('username') call would return a string, and getValue('age') would return a number. Attempting to set age to a string would result in a TypeScript compilation error.
Example 2: Optional Fields and Boolean
// Conceptual API
type SettingsFormDefinition = {
theme: 'light' | 'dark';
notificationsEnabled?: boolean; // Optional field
fontSize: number | null;
};
const settingsForm = new FormBuilder<SettingsFormDefinition>()
.addField('theme', 'light')
.addField('notificationsEnabled', false)
.addField('fontSize', null);
// Accessing values
const theme: 'light' | 'dark' = settingsForm.getValue('theme');
const notificationsEnabled: boolean | undefined = settingsForm.getValue('notificationsEnabled'); // Note: undefined if not set
// Setting values
settingsForm.setValue('theme', 'dark');
settingsForm.setValue('notificationsEnabled', true);
settingsForm.setValue('fontSize', 14);
settingsForm.setValue('fontSize', null);
// TypeScript should prevent:
// settingsForm.setValue('theme', 'blue'); // Error: Type '"blue"' is not assignable to type '"light" | "dark"'.
Output (Conceptual):
The settingsForm would have strongly typed accessors. notificationsEnabled would be inferred as boolean | undefined. Setting values would respect the defined union types.
Example 3: Basic Validation Errors
// Conceptual API
type LoginFormDefinition = {
email: string;
password: string;
};
const loginForm = new FormBuilder<LoginFormDefinition>()
.addField('email', '')
.addField('password', '');
// Assume an update function that also handles errors
loginForm.updateField('email', 'invalid-email', { error: 'Invalid email format' });
loginForm.updateField('password', 'weak', { error: 'Password too short' });
// A way to retrieve errors
const emailError: string | null = loginForm.getError('email'); // Should be 'Invalid email format'
const passwordError: string | null = loginForm.getError('password'); // Should be 'Password too short'
Output (Conceptual):
The form would also maintain an internal state for errors, keyed by field name. Retrieving an error for a field would return its associated error message (e.g., a string) or null if no error exists.
Constraints
- The core form definition and type inference must be handled purely within TypeScript's static type system.
- The implementation should aim for a clean and idiomatic TypeScript API.
- Avoid runtime JavaScript validation of types (e.g., using
typeofchecks on input values in a way that bypasses TypeScript's guarantees). The goal is compile-time type safety. - The library should be implementable within a single file or a small set of related files.
Notes
- Consider how you will represent the form's structure and its associated types. Generics will be a key tool here.
- Think about how to map field names (strings) to their corresponding types within the generic definition.
- For basic error handling, a simple mapping from field name to an error message (string) or
nullis sufficient. - The
FormBuildercould be a class, or you might explore functional approaches. The key is the resulting type safety. - Focus on the type-safe access and manipulation of form data as the primary objective.