Урок 10

Типизация форм

Простая форма

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 или проверку