Senior: питання на співбесіді з теми «TypeScript та інструменти»
Питання з реальних співбесід з відповідями: Laravel і PHP, бази даних, JavaScript і фронтенд, Git, Docker, API, безпека й архітектура. Тими самими темами, що й тести.
4 питання
Компоненти на кшталт списку, таблиці чи випадного списку працюють з даними будь-якого типу. Без узагальнень доводиться писати any - і зв'язок між даними та колбеками губиться.
Узагальнений компонент:
type ListProps<T> = {
items: T[];
getKey: (item: T) => string | number;
renderItem: (item: T) => ReactNode;
onSelect?: (item: T) => void;
};
function List<T>({ items, getKey, renderItem, onSelect }: ListProps<T>) {
return (
<ul>
{items.map((item) => (
<li key={getKey(item)} onClick={() => onSelect?.(item)}>
{renderItem(item)}
</li>
))}
</ul>
);
}
<List
items={users} // T = User - виведено з items
getKey={(user) => user.id}
renderItem={(user) => user.name} // user: User
onSelect={(user) => open(user.id)}
/>
TypeScript виводить T з переданих даних, і всі колбеки отримують правильний тип. Звернення до неіснуючого поля - помилка компіляції.
Обмеження типу параметра:
function Table<T extends { id: number }>({ rows, columns }: TableProps<T>) { /* ... */ }
Тепер можна використовувати row.id всередині й не передавати getKey.
Колонки, прив'язані до полів даних:
type Column<T> = {
key: keyof T;
header: string;
render?: (value: T[keyof T], row: T) => ReactNode;
};
key: keyof T дозволяє лише існуючі поля - перейменування поля в моделі одразу підсвітить усі таблиці, де воно використовується.
Синтаксис у .tsx: стрілкова функція const List = <T,>(props: ListProps<T>) => ... потребує коми після T - інакше <T> прочитається як JSX-тег. Оголошення function List<T>(...) цієї проблеми не має.
Явний параметр можна передати в JSX, якщо виведення неможливе: <List<User> items={[]} ... />.
Пастки:
memoчиforwardRefгубить узагальнення:memo(List)повертає компонент зT = unknown. Рішення - приведення типу (memo(List) as typeof List) або відмова від обгортки (з React Compiler потреба в ручномуmemoзменшується);- надто складні узагальнення (умовні типи, кілька параметрів) погіршують повідомлення про помилки - для команди простіший тип часто корисніший за максимально точний.
Звичайні необов'язкові props дозволяють будь-які комбінації, навіть безглузді:
type ButtonProps = {
href?: string; // посилання
onClick?: () => void; // кнопка
external?: boolean; // має сенс лише з href
};
<Button onClick={save} external /> // компілюється, хоча безглуздо
<Button /> // ні дії, ні посилання
Дискримінований union описує кожен допустимий варіант окремо:
type LinkProps = { as: 'link'; href: string; external?: boolean };
type ActionProps = { as: 'button'; onClick: () => void; disabled?: boolean };
type ButtonProps = (LinkProps | ActionProps) & { children: ReactNode };
function Button(props: ButtonProps) {
if (props.as === 'link') {
return (
<a href={props.href} target={props.external ? '_blank' : undefined}>
{props.children}
</a>
);
}
return (
<button onClick={props.onClick} disabled={props.disabled}>
{props.children}
</button>
);
}
<Button as="link" href="/jobs">Вакансії</Button>
<Button as="button" onClick={save}>Зберегти</Button>
<Button as="button" href="/x">...</Button> // помилка: href не існує для button
Варіант без явного дискримінатора - через never, щоб props взаємно виключали один одного:
type Props =
| { value: string; defaultValue?: never; onChange: (v: string) => void } // керований
| { defaultValue?: string; value?: never; onChange?: never }; // некерований
Передати одночасно value і defaultValue неможливо - типова помилка з полями форм.
Типові застосування:
- керований / некерований компонент;
- компонент стану запиту:
{ status: 'loading' } | { status: 'error'; error: string } | { status: 'success'; data: T }; - модальне вікно з різними наборами кнопок;
- поле форми, де
optionsобов'язкові лише для типуselect.
Пастки:
- деструктуризація в параметрах губить звуження:
function Button({ as, href, onClick })-hrefне існує у варіантіbutton, TypeScript не дасть деструктуризувати. Звужувати треба поprops.as, а деструктуризувати вже всередині гілки; ...restз union дає перетин полів і може «загубити» варіант - передаючи props далі, краще явно перелічувати;- повідомлення про помилки для великих union стають довгими - варто тримати варіантів небагато й давати їм імена.
Докладніше в документації: Звуження типів: дискриміновані union
Створення проєкту:
npm create vite@latest my-app -- --template react-ts
Шаблон містить vite.config.ts з плагіном @vitejs/plugin-react, tsconfig для коду застосунку й окремо для конфігурації, ESLint з eslint-plugin-react-hooks.
Що робить @vitejs/plugin-react:
- перетворює JSX (автоматичний runtime - імпорт
Reactу файлах не потрібен); - Fast Refresh - зміна компонента оновлює лише його, зі збереженням стану. Працює, якщо файл експортує лише компоненти: експорт констант чи функцій поряд з компонентом змушує перезавантажувати модуль повністю;
- у Vite 8 перетворення робить швидкий компілятор на Rust (Oxc), Babel за замовчуванням не потрібен.
React Compiler у поточній версії плагіна підключається двома способами:
// 1. через Babel - стабільний варіант
import react, { reactCompilerPreset } from '@vitejs/plugin-react';
import babel from '@rolldown/plugin-babel';
export default defineConfig({
plugins: [react(), babel({ presets: [reactCompilerPreset()] })],
});
// 2. нативний порт на Rust - експериментальний
export default defineConfig({
plugins: [react({ compiler: true })],
});
Компілятор 1.0 стабільний, працює з React 17+ і додає мемоізацію на етапі збирання. Для поступового впровадження - compilationMode: 'annotation' (компілюються лише функції з директивою "use memo").
TypeScript важливо розуміти правильно:
- Vite не перевіряє типи - він лише видаляє їх під час перетворення. Збирання пройде навіть з помилками типів;
- перевірка - окремим кроком:
tsc -bу скриптіbuild(так у шаблоні) і в CI, а під час розробки - редактор чиvite-plugin-checker; "jsx": "react-jsx","moduleResolution": "bundler","strict": true,"noUncheckedIndexedAccess"- корисні налаштування для нового проєкту;"isolatedModules": trueпотрібен, бо кожен файл перетворюється окремо:export typeдля реекспорту типів.
Змінні оточення - лише з префіксом VITE_, через import.meta.env; типи - в vite-env.d.ts.
ESLint з eslint-plugin-react-hooks (конфігурація recommended) у версії 7 містить і правила хуків, і правила React Compiler - порушення, через які компілятор пропустив би компонент, видно ще в редакторі.
З Laravel - laravel-vite-plugin поряд з плагіном React, сторінки через Inertia; стартовий набір Laravel React вже містить це налаштування.
eslint-plugin-react-hooks - офіційний плагін ESLint від команди React. З версії 6-7 він містить не лише два класичні правила хуків, а й правила, побудовані на аналізі React Compiler.
// eslint.config.js
import reactHooks from 'eslint-plugin-react-hooks';
import { defineConfig } from 'eslint/config';
export default defineConfig([
reactHooks.configs.flat.recommended,
]);
Класичні правила:
rules-of-hooks- хуки лише на верхньому рівні компонентів і власних хуків, не в умовах і циклах;exhaustive-deps- усі значення, використані вuseEffect/useMemo/useCallback, мають бути в залежностях.
Правила компілятора (у конфігурації recommended) ловлять порушення правил React, через які компонент працюватиме неправильно або компілятор його пропустить:
purity- нечисті виклики під час рендеру (Math.random(),Date.now()у тілі компонента);immutability- мутація props, стану чи значень з хуків;refs- читання чи записref.currentпід час рендеру;set-state-in-renderіset-state-in-effect- викликsetStateпрямо в рендері (нескінченний цикл) чи синхронно в ефекті (зайвий рендер; часто стан можна обчислити під час рендеру);static-components- оголошення компонента всередині іншого компонента (він перестворюється щоразу й губить стан);globals- зміна глобальних змінних під час рендеру;preserve-manual-memoization- ручнийuseMemo/useCallback, який компілятор не може зберегти;incompatible-library- бібліотеки з API, несумісним з мемоізацією компілятора;error-boundaries,use-memo,unsupported-syntax,config,gating.
Конфігурація recommended-latest додає експериментальні правила.
Чому це важливо навіть без компілятора: кожне з цих порушень - справжній баг або джерело зайвих рендерів незалежно від того, чи увімкнено компілятор. А з компілятором ESLint показує, які компоненти він пропустить і чому, - ще в редакторі.
Як упроваджувати в старий проєкт:
- увімкнути правила й подивитися кількість порушень - це оцінка, наскільки код готовий до компілятора;
- виправляти поступово, починаючи з
purity,immutability,refs- вони вказують на реальні помилки; - не вимикати правило коментарем без пояснення:
eslint-disableдляexhaustive-deps- найчастіше маскування справжньої проблеми із застарілими значеннями (stale closure).