PropTypes на користь TS.useState та useReducer): Дженерики (Generics) під капотом.React.MouseEvent, React.ChangeEvent).useRef): Різниця між MutableRefObject та RefObject.ComponentProps).Впровадження TypeScript у розробку React-додатків. Типізація пропсів, станів, подій та рефів. Переваги статичної типізації для великих команд та проєктів, що корелює з вимогами до якості ПЗ у комп’ютерних науках.
Сьогодні розробка Enterprise (корпоративних) веб-додатків мовою чистого, нетипізованого JavaScript вважається ознакою низької інженерної культури (Legacy-підхід). Динамічна типізація JS, яка історично дозволяла “швидко прототипувати”, на масштабах команди з 5+ осіб перетворюється на неконтрольоване джерело багів (найвідоміший з яких: Uncaught TypeError: Cannot read properties of undefined).
TypeScript став галузевим стандартом, вирішуючи цю проблему на етапі написання коду. Підтримка TypeScript у React є еталонною. Ця лекція присвячена правилам типізації React-компонентів, хуків та подій браузера, що дозволяє створювати надійні (Bulletproof) інтерфейси та зводити до мінімуму Runtime-помилки (помилки під час виконання).
JavaScript є мовою з динамічною слабкою типізацією. Тип змінної визначається не під час написання коду, а під час її обчислення рушієм браузера (V8/SpiderMonkey) у Runtime. Трансформація типів відбувається “на льоту” (наприклад, "5" + 2 стає рядок "52").
Проблема JS у React:
Якщо компонент очікує об’єкт user із полем firstName, а ми випадково передамо йому firstName з помилкою в назві ключа (first_name) чи взагалі null, браузер “впаде” з “Білим Екраном Смерті” (White Screen of Death) прямо на комп’ютері клієнта.
Рішення TypeScript: TS додає етап Компіляції (Transpilation). Він перевіряє всі типи у вашому коді в редакторі (IDE) до того, як код взагалі потрапить у браузер. Якщо типи не збігаються, програма просто не скомпілюється (Build Failed). Підсумок: TS переносить 80% багів з екрану клієнта безпосередньо в редактор розробника під час набору тексту.
До ери TypeScript у світі React домінувала бібліотека PropTypes, яка перевіряла типи в Runtime (що марнотратно впливало на продуктивність). З TS PropTypes стали застарілими.
Для опису очікуваних параметрів компонента (Props) ми використовуємо interface або type. З архітектурної точки зору в React вони майже ідентичні, але створення interface є кращою практикою для об’єктів.
// 1. Описуємо контракт (форму) пропсів
// Символ "?" означає, що поле опціональне (необов'язкове)
interface UserCardProps {
id: number;
name: string;
isAdmin?: boolean;
status: "online" | "offline" | "away"; // Union type (допустимі лише ці 3 значення)
}
// 2. Типізуємо параметри функції
const UserCard = ({ id, name, isAdmin = false, status }: UserCardProps) => {
return (
<div className={`card ${status}`}>
<h2>
{name} #{id}
</h2>
{isAdmin && <span className="badge">Admin</span>}
</div>
);
};
Якщо ви в іншій частині програми спробуєте використати <UserCard name="Alex" />, IDE підкреслить код червоним і відмовиться компілювати: Помилка: відсутній обов'язковий пропс 'id' та 'status'.
useState)Функція useState є так званою “Узагальненою функцією” (Type Generic) — вона приймає тип у кутових дужках <T>.
Патерн 1: Автоматичне виведення (Type Inference)
Якщо початкове значення примітивне (число, рядок, true/false), TypeScript володіє достатнім інтелектом, щоб вивести тип самостійно.
const [count, setCount] = useState(0);
// TS знає, що count - це number.
// Якщо ми напишемо setCount('hello'), TS видасть помилку.
Патерн 2: Явна типізація (Explicit Typing)
Необхідна, коли початкове значення є null, пустим масивом [] або складним об’єктом, тип якого TS не може “вгадати” з порожнечі.
interface User {
id: string;
email: string;
}
// Ми вказуємо, що стейт може бути АБО об'єктом User АБО null (Union Type)
const [currentUser, setCurrentUser] = useState<User | null>(null);
const handleLogin = (userData: User) => {
setCurrentUser(userData); // Працює
setCurrentUser({ id: "1" }); // Помилка: відсутнє поле email
};
Аналогічно типізується і useReducer, де ми строго описуємо структуру об’єкта State і лістинг всіх можливих варіантів дій (Action Discriminated Unions).
React.MouseEvent та React.ChangeEventТипізація колбеків (функцій зворотного виклику) для обробки подій DOM — найскладніша тема для початківців у TS.
Події в React не є рідними подіями браузера (Native Events). React огортає їх у власну кросбраузерну абстракцію — SyntheticEvent. Тому тип події потрібно брати з об’єкта React.
// Обов'язково вказуємо <HTMLButtonElement>, щоб TS знав, по чому клікнули
const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => {
event.preventDefault();
console.log(event.currentTarget.name);
};
return (
<button onClick={handleClick} name="submitBtn">
Натисни
</button>
);
Найважливіший кейс — контрольовані інпути. Отримуючи значення event.target.value, TypeScript повинен бути впевнений, що це значення витягується саме з тегу <input> (оскільки <div> не має атрибута value).
const [text, setText] = useState("");
// ChangeEvent типізується HTMLInputElement (або HTMLTextAreaElement для <textarea>)
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
setText(event.target.value);
};
return <input type="text" value={text} onChange={handleChange} />;
Лайфхак IDE: Якщо ви сумніваєтеся, який тип вказати, напишіть інлайн-функцію onChange={(e) => ...} прямо в JSX. Наведіть курсор миші на змінну e, і редактор VS Code (завдяки вбудованому серверу TypeScript) сам підкаже вам правильний тип.
useRef: Два обличчя ПосиланьХук useRef у React використовується для двох абсолютно різних задач, і TypeScript суворо вимагає правильної типізації для кожної з них (завдяки системі перевантаження функцій).
Сценарій А: Зберігання мутабельного значення (Мутабельний Ref) Використовується як еквівалент змінних екземпляра класу. Ми зберігаємо там таймери або будь-які змінні, які не повинні викликати рендер при їх зміні. Правило типізації: Передаємо початкове значення, і воно визначає тип.
// TS виведе MutableRefObject<number>. Властивість .current можна змінювати.
const timerId = useRef<number>(0);
useEffect(() => {
timerId.current = window.setInterval(() => console.log("Tick"), 1000);
return () => clearInterval(timerId.current);
}, []);
Сценарій Б: Прямий доступ до DOM-елемента (Read-only Ref)
До цього ми зверталися лише по закінченні монтування компонента.
Правило типізації: Тип дженерика МАЄ бути відповідним HTML-елементом, а початкове значення ОБОВ’ЯЗКОВО null.
// TS виведе RefObject<HTMLInputElement>. Додавання null забороняє мутації .current об'єкта
const inputRef = useRef<HTMLInputElement>(null);
const focusInput = () => {
// Ми ставимо ?. (Optional Chaining), бо під час першого рендеру .current ще дорівнює null
inputRef.current?.focus();
};
return <input ref={inputRef} />;
Часто виникає ситуація, коли ми створюємо власний компонент, наприклад, кастомну кнопку <SubmitButton>, яка повинна приймати всі ті самі параметри, що й звичайний тег <button> в HTML (disabled, type, onClick, onBlur), плюс кілька наших власних.
Писати цей інтерфейс вручну недоцільно. Ми використовуємо вбудовану утиліту TS — React.ComponentProps (або успадкування інтерфейсу).
// Розширюємо (extends) інтерфейс нашого компонента всіма властивостями звичайної кнопки
interface CustomButtonProps extends React.ComponentProps<"button"> {
isLoading?: boolean; // Додаємо наше власне кастомне поле
}
// За допомогою Rest-оператора (...restProps) збираємо всі параметри крім isLoading
const CustomButton = ({
isLoading,
children,
...restProps
}: CustomButtonProps) => {
return (
// Прокидаємо системні пропси на реальний DOM-вузол
<button
{...restProps}
className="btn-primary"
disabled={isLoading || restProps.disabled}
>
{isLoading ? "Завантаження..." : children}
</button>
);
};
// Використання (TS тепер дозволяє передати type і onMouseEnter):
<CustomButton type="submit" isLoading={true} onMouseEnter={() => alert(1)}>
Зберегти
</CustomButton>;
Це патерн інкапсуляції, який створює враження, що розробник працює з нативними компонентами браузера (безперешкодний DX - Developer Experience).
interface) діють як строгий контракт (Contract-driven Development). Якщо дочірній компонент вимагає id як число, TS не дозволить батьку передати його у вигляді рядка ("1").React.MouseEvent, React.ChangeEvent) та посилань на DOM-вузли (useRef<HTMLDivElement>) ми делегуємо компілятору розуміння екосистеми браузера (DOM API), що робить процес ін’єкцій безпечним і забезпечує 100% автодоповнення (IntelliSense) коду.useState<T> необхідне для коректної обробки відкладеного або асинхронного завантаження стейту (переходи зі стану null у стан повністю заповненого Об’єкта).ComponentProps) є базовим патерном при створенні надійних “Дизайн-систем” рівня UI-бібліотек, коли кастомний компонент безшовно інтегрує свої вимоги з нативними вимогами HTML-стандартів.PropTypes або ручні перевірки typeof x === 'string' всередині рендеру) програє концепції Compile-time перевірки (через TypeScript) при розгортанні великих Production-додатків?useState у ситуаціях, коли початкове значення є порожнім об’єктом, і коли воно є скалярним(примітивним) значенням 0. В якому з випадків TypeScript потребує явного вказування типу <T> і чому?React.ChangeEvent<HTMLInputElement>. Поясніть з інженерної точки зору, чому розробники React не дозволяють просто написати рідний тип браузера Event, і що означає префікс Synthetic в об’єкті події.useRef? (Підказка: роль початкового значення null).React.ComponentProps<'input'> під час створення кастомного текстового поля <AppInput/>, в яке обгорнутий нативний HTML-тег? Яку архітектурну проблему вирішує цей патерн?