Form Typing Patterns
On this page
Form State Is Not Domain State
- Form inputs are drafts and can be incomplete.
- Domain models represent validated invariants.
- Production rule: never reuse domain types for draft input state.
Recommended Modeling
- FormState: all fields as strings or input friendly types.
- Domain: validated types used by business logic and API calls.
- Errors: structured map keyed by field name, plus a global message.
Example: Draft vs Domain
type SignupFormState = {
email: string;
password: string;
};
type SignupDomain = {
email: string;
passwordHash: string;
};
type FieldErrors = Partial<Record<keyof SignupFormState, string>>;
Typed Validation
function validateDraft(d: SignupFormState): { ok: true; value: SignupFormState } | { ok: false; errors: FieldErrors } {
const errors: FieldErrors = {};
if (!d.email.includes("@")) errors.email = "Invalid email";
if (d.password.length < 8) errors.password = "Minimum length is 8";
if (Object.keys(errors).length > 0) return { ok: false, errors };
return { ok: true, value: d };
}
Submit Pipeline
- Validate draft.
- Map to API payload.
- Send request with in flight guard.
- Normalize server errors back into FieldErrors.
Failure Modes
- Using Partial for domain types and allowing missing required fields.
- Errors stored as unstructured strings that cannot map to fields.
- Client validation differs from server, producing confusing UX.
- Form state too broad causing re render lag.
Production Checklist
- Form draft types are separate from domain models.
- Validation produces structured errors.
- Submit is guarded against double submit.
- Server errors map to fields consistently.