TypeScript playground or ts-node
Create a type-safe form validation library using mapped types.
1 // Map data type to its validation errors type 2 type ValidationErrors<T> = { 3 [K in keyof T]?: string; 4 }; 5 6 // Map data type to touched state 7 type TouchedFields<T> = { 8 [K in keyof T]?: boolean; 9 }; 10 11 // Complete form state 12 type FormState<T> = { 13 values: T; 14 errors: ValidationErrors<T>; 15 touched: TouchedFields<T>; 16 isValid: boolean; 17 isDirty: boolean; 18 }; 19 20 // Validators — Record<keyof T, (val: T[K]) => string | undefined> 21 type Validators<T> = { 22 [K in keyof T]?: (value: T[K]) => string | undefined; 23 }; 24 25 // Simple useForm implementation 26 function useForm<T extends Record<string, unknown>>( 27 initialValues: T, 28 validators: Validators<T> 29 ): { 30 state: FormState<T>; 31 setValue<K extends keyof T>(key: K, value: T[K]): FormState<T>; 32 touch(key: keyof T): void; 33 } { 34 let state: FormState<T> = { 35 values: { ...initialValues }, 36 errors: {}, 37 touched: {}, 38 isValid: true, 39 isDirty: false, 40 }; 41 42 function validate(values: T): ValidationErrors<T> { 43 const errors: ValidationErrors<T> = {}; 44 for (const key in validators) { 45 const validator = validators[key]; 46 if (validator) { 47 const error = validator(values[key]); 48 if (error) (errors as any)[key] = error; 49 } 50 } 51 return errors; 52 } 53 54 return { 55 state, 56 setValue(key, value) { 57 state.values = { ...state.values, [key]: value }; 58 state.errors = validate(state.values); 59 state.isValid = Object.keys(state.errors).length === 0; 60 state.isDirty = true; 61 return state; 62 }, 63 touch(key) { state.touched = { ...state.touched, [key]: true }; }, 64 }; 65 } 66 67 // Usage 68 interface LoginData { email: string; password: string; } 69 const form = useForm<LoginData>( 70 { email: '', password: '' }, 71 { 72 email: (v) => v.includes('@') ? undefined : 'Invalid email', 73 password: (v) => v.length >= 8 ? undefined : 'Min 8 chars', 74 } 75 ); 76 77 const state = form.setValue('email', 'not-an-email'); 78 console.log(state.errors.email); // 'Invalid email' 79 console.log(state.errors.password); // 'Min 8 chars' 80 console.log(state.isValid); // false
useForm<LoginData> returns { errors: ValidationErrors<LoginData> } with typed fieldsSign in to share your feedback and join the discussion.