Типизация форм
Простая форма
type FormValues = {
name: string;
email: string;
age: number;
};
function Form() {
const [values, setValues] = useState<FormValues>({
name: '',
email: '',
age: 0,
});
function handleChange(e: ChangeEvent<HTMLInputElement>) {
const { name, value, type } = e.target;
setValues((prev) => ({
...prev,
[name]: type === 'number' ? Number(value) : value,
}));
}
function handleSubmit(e: FormEvent<HTMLFormElement>) {
e.preventDefault();
console.log(values);
}
return (
<form onSubmit={handleSubmit}>
<input name="name" value={values.name} onChange={handleChange} />
<input name="email" value={values.email} onChange={handleChange} />
<input name="age" type="number" value={values.age} onChange={handleChange} />
<button>Отправить</button>
</form>
);
}
Проблема с [name]
setValues((prev) => ({
...prev,
[name]: value, // ошибка: string не подходит к number
}));
TypeScript не знает тип конкретного поля. Решения:
Отдельные обработчики
<input value={values.age} onChange={(e) => setValues({ ...values, age: Number(e.target.value) })} />
Общий с typeof-проверкой
function handleChange<K extends keyof FormValues>(key: K, value: FormValues[K]) {
setValues((prev) => ({ ...prev, [key]: value }));
}
<input onChange={(e) => handleChange('age', Number(e.target.value))} />
K extends keyof FormValues — ключ, FormValues[K] — точный тип значения.
Generic-форма
function useForm<T extends Record<string, unknown>>(initial: T) {
const [values, setValues] = useState(initial);
function setValue<K extends keyof T>(key: K, value: T[K]) {
setValues((prev) => ({ ...prev, [key]: value }));
}
return { values, setValue };
}
const { values, setValue } = useForm({ name: '', age: 0 });
setValue('name', 'Алиса'); // ок
setValue('age', 25); // ок
setValue('age', '25'); // ошибка
Ошибки формы
type FormErrors<T> = Partial<Record<keyof T, string>>;
type UserForm = { name: string; email: string };
type UserErrors = FormErrors<UserForm>;
// { name?: string; email?: string }
Валидация
function validate(values: FormValues): FormErrors<FormValues> {
const errors: FormErrors<FormValues> = {};
if (!values.name) errors.name = 'Имя обязательно';
if (!values.email.includes('@')) errors.email = 'Некорректный email';
return errors;
}
React Hook Form
type FormValues = {
email: string;
password: string;
};
const { register, handleSubmit, formState: { errors } } = useForm<FormValues>({
defaultValues: { email: '', password: '' },
});
<form onSubmit={handleSubmit(onSubmit)}>
<input {...register('email')} />
{errors.email && <p>{errors.email.message}</p>}
</form>
register('email') — типобезопасен: несуществующее поле — ошибка.
Zod + React Hook Form
import { z } from 'zod';
import { zodResolver } from '@hookform/resolvers/zod';
const schema = z.object({
email: z.string().email('Некорректный email'),
password: z.string().min(8, 'Минимум 8 символов'),
});
type FormValues = z.infer<typeof schema>;
const { register, handleSubmit, formState: { errors } } = useForm<FormValues>({
resolver: zodResolver(schema),
});
Тип выведен из схемы. Валидация и тип — из одного источника.
Типизация чекбоксов
const [agreed, setAgreed] = useState(false);
<input
type="checkbox"
checked={agreed}
onChange={(e) => setAgreed(e.target.checked)}
/>
e.target.checked — boolean.
Select
const [role, setRole] = useState<'user' | 'admin'>('user');
<select value={role} onChange={(e) => setRole(e.target.value as 'user' | 'admin')}>
<option value="user">Пользователь</option>
<option value="admin">Админ</option>
</select>
as нужен, потому что e.target.value — string, а не union. Альтернатива — обработчик:
function handleRole(e: ChangeEvent<HTMLSelectElement>) {
const value = e.target.value;
if (value === 'user' || value === 'admin') {
setRole(value);
}
}
Итоги
- Тип формы — через
type keyofиT[K]для безопасного обновления полейFormErrors<T>для ошибок- React Hook Form — типобезопасен
- Zod + resolver — типы из схемы
e.target.checkedдля чекбоксов- Union для select через
asили проверку