Современная фронтенд-разработка почти немыслима без TypeScript, но интеграция статической типизации в React-компоненты регулярно преподносит сюрпризы даже опытным разработчикам. Рассмотрим три классических сценария, где неправильная типизация приводит к скрытым багам, и способы выхода из этих ловушек.
1. Состояния с undefined: Кажущаяся опциональность
const [user, setUser] = useState<User>();
Этот код работает, но таит проблему: TypeScript разрешает undefined
даже при включенном strictNullChecks
. Для состояния, обязательного после монтирования, лучше явно указать тип:
const [user, setUser] = useState<User | null>(null);
// При использовании:
if (!user) return <Loader />;
return <Profile data={user} />; // Без ошибок - тип User
Пересечение типов T | null
вместо T | undefined
сохраняет строгую проверку, особенно когда состояние инициализируется асинхронно.
2. Дети как функция: Типизация render props
Неверное определение типа для компонентов с render props:
type DropdownProps = {
children: (isOpen: boolean) => ReactNode;
};
const Dropdown = ({children}: DropdownProps) => {
const [isOpen, setIsOpen] = useState(false);
return children(isOpen);
};
Проблема в том, что ReactNode
включает string | number | boolean | ...
, хотя технически функция возвращает ReactElement
. Для точной типизации:
children: (isOpen: boolean) => JSX.Element;
Но для поддержки массивов и фрагментов:
children: (isOpen: boolean) => React.ReactElement | React.ReactElement[];
3. События форм: Тип для onSubmit
Обычная ошибка при работе с формами:
const handleSubmit = (e: Event) => {
e.preventDefault(); // Ошибка: свойство есть только у React.FormEvent
// ...
};
Правильный тип зависит от источника события:
// Нативный элемент формы (не React):
const handleNativeSubmit = (e: SubmitEvent) => { ... };
// React-компонент:
const handleReactSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
const email = (e.currentTarget.elements.namedItem('email') as HTMLInputElement).value;
};
Для универсального использования с пользовательскими компонентами:
type FormSubmitHandler<T extends HTMLElement> = (
e: React.FormEvent<T>
) => void;
4. Дженерики для асинхронных операций
Типизация API-запросов с параметрами:
const fetchData = async <T,>(url: string): Promise<T> => {
const res = await fetch(url);
return res.json(); // Тип T, но нужна проверка
};
Улучшенная версия с проверкой схемы через Zod:
const fetchValidatedData = async <T,>(
url: string,
schema: z.ZodSchema<T>
): Promise<T> => {
const res = await fetch(url);
const data = await res.json();
return schema.parse(data); // Выбрасывает ошибку при несоответствии
};
// Использование:
const userSchema = z.object({
id: z.string(),
name: z.string(),
});
const user = await fetchValidatedData('/api/user', userSchema);
Этот подход сочетает статическую типизацию с валидацией в рантайме, исключая расхождения между TS-типами и реальными данными.
Практические рекомендации
- Для состояний компонентов предпочитайте
null
вместоundefined
как стартовое значение - Используйте
JSX.Element
вместоReactNode
для функциональных детей - Заменяйте
any
в ивент-хэндлерах на конкретныеReact.MouseEvent<HTMLButtonElement>
- Для сложных API-интерфейсов применяйте дженерики с валидацией схем данных
- Всегда включайте
strict: true
иstrictNullChecks
в tsconfig.json
Статическая типизация в React — это не про добавление аннотаций «для галочки». Каждый тип должен быть механизмом предупреждения логических ошибок: если приходится использовать @ts-ignore
или as any
, это сигнал к пересмотру архитектуры компонента, а не к подавлению проверок. Типы создают контракты между частями приложения — чем точнее эти контракты, тем меньше неожиданностей при разработке и рефакторинге.