Junior: питання на співбесіді з теми «TypeScript та інструменти»
Питання з реальних співбесід з відповідями: Laravel і PHP, бази даних, JavaScript і фронтенд, Git, Docker, API, безпека й архітектура. Тими самими темами, що й тести.
4 питання
Props компонента типізують звичайним типом чи інтерфейсом у параметрі функції:
type ButtonProps = {
label: string;
variant?: 'primary' | 'secondary'; // необов'язковий
onClick: () => void;
};
function Button({ label, variant = 'primary', onClick }: ButtonProps) {
return <button className={variant} onClick={onClick}>{label}</button>;
}
Значення за замовчуванням задають деструктуризацією - окремий defaultProps для функціональних компонентів у React 19 більше не підтримується.
children оголошують явно - як і будь-який інший prop:
import type { ReactNode } from 'react';
type CardProps = {
title: string;
children: ReactNode;
};
ReactNode чи ReactElement:
ReactNode- усе, що React уміє відрендерити: JSX, рядки, числа,bigint,null,undefined,boolean, масиви цього, а також проміси (дляuse). Правильний тип дляchildrenу більшості випадків;ReactElement- лише результат JSX (<div />,<Card />). Рядок чиnullтуди не передати. Доречний, коли компонент справді приймає рівно один елемент (наприклад, щоб клонувати його чи додати props).
<Card title="Профіль">Текст</Card> // ReactNode - ок
<Tooltip content="Підказка"><button /></Tooltip> // children: ReactElement
React.JSX.Element - тип, який повертає JSX-вираз. У React 19 глобального простору імен JSX більше немає: пишуть React.JSX.Element або імпортують JSX з react. Тип повернення компонента зазвичай не вказують - TypeScript виведе його сам.
React.FC у сучасному коді вживають рідко: він не додає користі порівняно з типізованим параметром, а історично неявно додавав children. Звичайна функція з типізованими props - стандарт.
Пастка: children: JSX.Element забороняє передати текст чи кілька елементів - помилка компіляції там, де її не очікують.
Докладніше в документації: TypeScript у React: типізація children
React передає в обробники синтетичні події - обгортки над подіями браузера з однаковою поведінкою в усіх браузерах. Для кожного виду події є свій тип, параметризований елементом.
Інлайн-обробник типізувати не треба - TypeScript виведе тип сам:
<input onChange={(event) => setName(event.target.value)} />
// event: React.ChangeEvent<HTMLInputElement>
Окрема функція потребує явного типу:
import type { ChangeEvent, FormEvent, MouseEvent } from 'react';
function handleChange(event: ChangeEvent<HTMLInputElement>) {
setEmail(event.target.value);
}
function handleSubmit(event: FormEvent<HTMLFormElement>) {
event.preventDefault();
}
function handleClick(event: MouseEvent<HTMLButtonElement>) {
console.log(event.clientX);
}
Найчастіші типи: ChangeEvent, FormEvent, MouseEvent, KeyboardEvent, FocusEvent, DragEvent, ClipboardEvent.
Тип самого обробника - зручно для props:
type SearchProps = {
onSearch: (query: string) => void; // власний колбек
onKeyDown?: React.KeyboardEventHandler<HTMLInputElement>; // обробник DOM-події
};
KeyboardEventHandler<T> - це (event: KeyboardEvent<T>) => void.
Як дізнатися правильний тип: навести курсор на onChange в JSX - редактор покаже очікувану сигнатуру.
target чи currentTarget:
currentTarget- елемент, на якому висить обробник, і його тип відомий (HTMLButtonElement);target- елемент, де виникла подія (може бути вкладений<span>усередині кнопки), тому дляMouseEventвін має загальний типEventTarget.
Для полів введення event.target.value працює, бо в ChangeEvent<HTMLInputElement> тип target уточнено.
Пастки:
- власні колбеки не варто типізувати як події DOM:
onSelect: (event) => ...з передачею всього об'єкта події змушує батька знати про DOM. Краще передавати значення:onSelect(item.id); anyдля події вимикає перевірку доступу до полів - друкарська помилкаevent.target.vauleпройде непоміченою.
TypeScript виводить тип стану з початкового значення, і найчастіше явний тип не потрібен:
const [count, setCount] = useState(0); // number
const [name, setName] = useState(''); // string
const [open, setOpen] = useState(false); // boolean
Коли виведення не спрацьовує:
1. Початкове null - значення з'явиться пізніше:
type User = { id: number; name: string };
const [user, setUser] = useState<User | null>(null);
if (user) {
user.name; // після перевірки - User
}
Без явного типу useState(null) дасть тип null, і setUser(data) не скомпілюється.
2. Порожній масив:
const [items, setItems] = useState<Product[]>([]);
Без параметра useState([]) дає never[] - у такий масив нічого не додати.
3. Обмежений набір значень (union):
type Status = 'idle' | 'loading' | 'success' | 'error';
const [status, setStatus] = useState<Status>('idle');
Без явного типу стан стане string, і setStatus('sucess') з помилкою пройде.
Стани, що залежать один від одного, краще поєднати в один з дискримінованим union - тоді неможливі комбінації не скомпілюються:
type RequestState =
| { status: 'loading' }
| { status: 'success'; data: User[] }
| { status: 'error'; error: string };
const [state, setState] = useState<RequestState>({ status: 'loading' });
if (state.status === 'success') {
state.data; // доступно лише тут
}
Окремі isLoading, error, data дозволяють стан «завантажується, але з помилкою й даними», а union - ні.
Функція оновлення типізується автоматично: setCount((c) => c + 1) знає, що c - число.
Лінива ініціалізація теж виводить тип з результату: useState(() => readFromStorage()).
Пастка: useState<User>({} as User) - «обіцянка» компілятору, що порожній об'єкт - повноцінний User. Помилки звернення до відсутніх полів з'являться під час виконання, а не компіляції. Чесніше User | null.
<StrictMode> - обгортка, яка в режимі розробки вмикає додаткові перевірки. У продакшен-збірці вона нічого не робить і нічого не коштує.
createRoot(document.getElementById('root')!).render(
<StrictMode>
<App />
</StrictMode>,
);
Шаблони Vite і більшості фреймворків вмикають її за замовчуванням.
Що вона робить:
1. Рендерить компоненти двічі. Функція компонента, ініціалізатори useState, функції useMemo та оновлювачі стану викликаються по два рази. Якщо результат різний - компонент нечистий: змінює щось зовнішнє під час рендеру.
function List({ items }) {
items.push({ id: 'extra' }); // мутація props - у StrictMode побачите два 'extra'
return items.map(...);
}
2. Запускає ефекти двічі при монтуванні: setup → cleanup → setup. Так React перевіряє, що ефект правильно прибирає за собою:
useEffect(() => {
const connection = createConnection(roomId);
connection.connect();
return () => connection.disconnect(); // без цього - два з'єднання
}, [roomId]);
Якщо ефект ламається від повторного запуску, він зламається й у реальному житті - при поверненні на сторінку, швидкому оновленні (Fast Refresh) чи в майбутніх можливостях React, що зберігають стан при прихованні компонентів.
3. Перевіряє колбеки ref так само: прив'язка - відв'язка - прив'язка.
4. Попереджає про застарілі API.
Типові «симптоми» в розробці:
console.logу компоненті друкує двічі (React DevTools можуть приглушувати друге виведення);- запит у
useEffectйде двічі - це нормально в розробці. Якщо він не скасовується й дає дублікати даних (два однакові записи черезPOSTв ефекті), проблема в коді, а не в StrictMode; - лічильник, збільшений в ефекті без очищення, показує 2.
Чого НЕ робити: вимикати StrictMode, щоб «прибрати подвійний запит». Правильне рішення - очищення в ефекті, скасування запиту (AbortController) або завантаження даних засобами фреймворку чи бібліотекою на кшталт TanStack Query.
Додатково: React Compiler і правила ESLint eslint-plugin-react-hooks знаходять ті самі порушення чистоти ще до запуску.