Увійти Реєстрація
Блог Серії
Кар'єра
Вакансії Компанії
Навчання
Документація Співбесіди Тестування Відео
Екосистема
Пакети Ресурси Проєкти Інструменти Події
Інше
Про нас Реклама

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 у React: події DOM

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.

Докладніше в документації: TypeScript у React: useState

<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 знаходять ті самі порушення чистоти ще до запуску.

Докладніше в документації: StrictMode