Поширені помилки TypeScript у React та способи їх вирішення
Практичний огляд п’яти поширених категорій помилок TypeScript у додатках на React — props, події, стан, асинхронні дані та дочірні елементи — із чіткими способами їх виправлення.
Якщо ви прийшли з JavaScript, TypeScript спочатку може здатися складним у використанні. Протягом перших кількох днів може здаватися, що компілятор активно працює проти вас: всюди червоні та жовті лінії, загадкові повідомлення, які, здається, нікуди не ведуть... і в певний момент ви можете почати сумніватися, чи справді було правильно перейти на TypeScript, думаючи, чи не простіше було б повернутися до звичайного JavaScript.
Але справа у тому: впровадження TypeScript у кодову базу React — це один із найрозумніших кроків, які ви можете зробити, якщо хочете підтримувати якість коду з часом.
А ті помилки та попередження, на які ви постійно натрапляєте? Вони абсолютно варті того, щоб долати їх; вам просто потрібно навчитися розпізнавати закономірності, які за ними стоять.
Більшість проблем з TypeScript, з якими ви стикаєтесь під час розробки додатків на React, зовсім не є випадковими. Ті самі кілька типів помилок з’являються знову та знову у різних проектах, і як тільки ви кілька разів їх вирішите, почнете помічати їх миттєво та точно розуміти, що вони означають. У цій статті розглядаються п’ять категорій помилок, які зустрічаються майже в кожному проекті на React із використанням TypeScript, пояснюється, чому виникає кожна з них, та як їх усунути.
Одна порада перед початком: завжди читайте повне повідомлення про помилку. Помилки TypeScript на перший погляд можуть здатися страшними, але насправді вони мають досить передбачувану структуру. Не дозволяйте великій кількості тексту збити вас з пантелику, і якщо ви коли-небудь не будете впевнені, з чого почати, останній рядок повідомлення зазвичай є найкориснішою частиною, на яку варто звернути увагу.
З урахуванням цього давайте почнемо.
1. Помилки властивостей компонентів
Пропси є основою кожного компонента React, тому цілком логічно, що помилки типу, пов’язані з пропсами, зазвичай є першими, з якими стикаються розробники.
Помилка: Властивість ‘X’ не існує у типі '{}'.
Це виникає, коли ви використовуєте пропс у своєму компоненті, не визначивши для нього тип.
// This won't give any errors in JavaScript...
function UserCard({ name, email }) {
return (
<div>
<h3>{name}</h3>
<p>{email}</p>
</div>
);
};
// But in TypeScript...
// Error: Parameter 'name' implicitly has an 'any' type
// Error: Parameter 'email' implicitly has an 'any' type
Ця помилка з’являється просто тому, що пропсам ніколи не були вказані явні типи.
Рішення: оголосіть інтерфейс, який описує ваші пропси.
interface UserCardProps {
name: string;
email: string;
};
function UserCard({ name, email}: UserCardProps) {
return (
<div>
<h3>{name}</h3>
<p>{email}</p>
</div>
);
};
Помилка: Тип ‘string’ не може бути присвоєний типу ‘number’.
Ця проблема досить проста: це означає, що ви передали пропс із значенням неправильного типу.
Рішення: перевірте тип значення, яке ви передаєте у пропс.
interface ItemForSaleProps {
imgUrl: string;
itemName: string;
amount: number;
currency: string;
};
function ItemForSale({
itemName,
imgUrl,
amount,
currency
}: ItemForSaleProps) {
return (
<div class="item-for-sale">
<img src={imgUrl} alt="item image" />
<h5>{itemName}</h5>
<p>{`${currency} ${amount.toFixed(2)}`}</p>
</div>
);
};
// Error happens when passing a string where a number is expected.
<ItemForSale
imgUrl="api.example.com/image"
itemName="Sample"
amount="10.99"
currency="USD"
/>
// Should be like this
<ItemForSale
imgUrl="api.example.com/image"
itemName="Sample"
amount={10.99}
currency="USD"
/>
Чи помічаєте ви різницю між цими двома версіями? Компонент ItemForSale очікує, що параметр amount буде типу number, тому передача рядка спричиняє помилку, тоді як передача справжнього числового значення є правильною. Це саме те, що робить TypeScript – виявляє потенційні проблеми ще до того, як код почне виконуватися. У цьому прикладі виклик методу .toFixed(2) для рядка '10.99' фактично призведе до збою під час виконання, але TypeScript вказує на проблему під час компіляції.
Помилка: У типі '{}' відсутня властивість 'X', яка є обов’язковою у типі 'Props'.
Це трапляється тоді, коли ваший інтерфейс позначає певну властивість як обов’язкову, але ви забуваєте її передати під час використання компонента.
interface CustomButtonProps {
label: string;
onClick: () => void;
};
function CustomButton({ label, onClick }: CustomButtonProps) {
return <button onClick={onClick}>{label}</button>;
};
// Bad usage:
<CustomButton label="Submit" />
// Error: Property 'onClick' is missing in type '{ label: string; }'
// but required in type 'ButtonProps'
Рішення: ще раз перевірте як визначення параметрів у вашому компоненті, так і ті параметри, які ви фактично передаєте під час їх відображення.
// Correct usage:
<CustomButton label="Submit" onClick={handleSubmit} />
Іноді у вас бувають параметри, які не завжди потрібні. У таких випадках ви можете позначити параметр як необов’язковий за допомогою ?. Пам’ятайте, що коли ви робите параметр необов’язковим, вам також слід надати або значення за замовчуванням, або якийсь механізм перевірки на null, щоб безпечно обробляти його відсутність.
interface CustomButtonProps {
label: string;
onClick: () => void;
disabled?: boolean; // this prop is now optional
className?: string; // this one too
};
function CustomButton({ label, onClick, disabled = false, className }: CustomButtonProps) {
return (
<button
onClick={onClick}
disabled={disabled}
className={className}
>
{label}
</button>
);
};
// This component now works with or without the optional props
<CustomButton label="Submit" onClick={handleSubmit} />
<CustomButton label="Submit" onClick={handleSubmit} disabled={true} />
Тепер, коли існують необов’язкові параметри, є ще одна помилка, про яку варто згадати.
Помилка: тип ‘X | undefined’ не може бути присвоєний типу ‘X’.
Роблячи параметр необов’язковим, автоматично додається undefined до його типу, що створює проблеми, якщо ви намагаєтесь використати це значення, не перевіривши спочатку його наявність.
interface ProfileProps {
name: string;
bio?: string;
};
function Profile({ name, bio }: ProfileProps) {
return (
<p>{name}</p>
<p>{bio.toUpperCase()}</p>
// Error: Object is possibly 'undefined'
);
};
Загалом існує три способи вирішити цю проблему:
Варіант 1: вказати значення за замовчуванням під час розбирання props.
// bio is always a string, will render a default text when there's
// no bio provided
function Profile({ name, bio = 'No bio available' }: ProfileProps) {
return (
<p>{name}</p>
<p>{bio.toUpperCase()}</p>
);
};
Варіант 2: скористатися опційним ланцюгуванням.
// returns undefined if bio is undefined
function Profile({ name, bio }: ProfileProps) {
return (
<p>{name}</p>
<p>{bio?.toUpperCase()}</p>
);
};
Варіант 3: використати умовне відображення.
// will only render if bio exists
function Profile({ name, bio }: ProfileProps) {
return (
<p>{name}</p>
<p>{bio && bio.toUpperCase()}</p>
);
};
2. Помилки обробників подій
Будь-який компонент React, який реагує на дії користувача, потребує обробників подій, а TypeScript надає дуже точні типи для кожного виду подій DOM. Неправильне використання цих типів є однією з найпоширеніших причин плутанини у розробників, які працюють із типованим кодом React.
Проблема: параметр ‘e’ неявно має тип ‘any’.
Це виникає, коли ви пишете обробник подій як окрему функцію поза JSX та забуваєте позначити параметр події.
// Error: Parameter 'e' implicitly has an 'any' type
const handleClick = (e) => {
e.preventDefault();
};
Рішення: Додайте відповідний тип події React до параметра.
const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {
e.preventDefault();
};
Варто зазначити: коли ви пишете обробник безпосередньо всередині JSX, TypeScript може самостійно визначити тип, тому у такому випадку ця конкретна помилка не з’явиться.
<button onClick={(e) => {
e.preventDefault() // e is automatically React.MouseEvent<HTMLButtonElement>
}}>
Click here
</button>
Проблема: Властивість 'value' відсутня у типі 'EventTarget'.
Це, ймовірно, найпоширеніша помилка TypeScript серед розробників React — вона також є однією з найскладніших для розуміння при першій зустрічі. Вона з’являється, коли ви намагаєтесь прочитати e.target.value усередині обробника змін.
// This will give an error because TS doesn't know target is an input element.
const handleChange = (e: React.ChangeEvent) => {
console.log(e.target.value);
// Error: Property 'value' does not exist on type 'EventTarget'
};
Основною причиною є те, що EventTarget — це універсальний інтерфейс DOM, тому TypeScript не може знати, що в цьому конкретному обробнику об’єктом є елемент <input>, який випадково має властивість value.
Рішення: Вказати конкретний тип елемента як параметр універсалізації для типу події.
// TypeScript now knows the target is an HTMLInputElement
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
console.log(e.target.value);
};
Той самий підхід працює і з іншими елементами — для елемента select, наприклад, потрібно замінити HTMLInputElement на HTMLSelectElement. Ось короткий довідник з найпоширеніших типів подій, які ви будете використовувати:
// Click events
onClick: (e: React.MouseEvent<HTMLButtonElement>) => void
onClick: (e: React.MouseEvent<HTMLDivElement>) => void
// Input change events
onChange: (e: React.ChangeEvent<HTMLInputElement>) => void
onChange: (e: React.ChangeEvent<HTMLSelectElement>) => void
onChange: (e: React.ChangeEvent<HTMLTextAreaElement>) => void
// Form submission
onSubmit: (e: React.FormEvent<HTMLFormElement>) => void
// Keyboard events
onKeyDown: (e: React.KeyboardEvent<HTMLInputElement>) => void
onKeyUp: (e: React.KeyboardEvent<HTMLInputElement>) => void
// Focus events
onFocus: (e: React.FocusEvent<HTMLInputElement>) => void
onBlur: (e: React.FocusEvent<HTMLInputElement>) => void
Оскільки ми говоримо про обробники подій, ви можете запитатися, як правильно їх задати під час передачі як пропсів до дочірніх компонентів. У такому випадку їх слід описувати як функції, які приймають відповідний об’єкт події.
interface SearchInputProps {
onSearch: (e: React.ChangeEvent<HTMLInputElement>) => void;
onSubmit: (e: React.FormEvent<HTMLFormElement>) => void;
};
function SearchInput({ onSearch, onSubmit }: SearchInputProps) {
return (
<form onSubmit={onSubmit}>
<input type="text" onChange={onSearch} />
<button type="submit">Search</button>
</form>
);
};
А якщо певному обробнику зовсім не потрібно отримувати подію, ви можете відповідним чином спростити його тип.
interface SearchInputProps {
onClick: () => void;
onHover: () => void;
};
3. useState та useRef
Hooks знаходяться в центрі сучасного розроблення React, тому не дивно, що useState та useRef мають свої особливості у TypeScript.
Проблема: Аргумент типу ‘X’ не може бути присвоєний параметру типу ‘never’.
Це одна з найбільш заплутаних помилок, з якими можна зіткнутися. Вона виникає, коли ви ініціалізуєте useState порожнім масивом, через що TypeScript визначає тип стану як never[]. Ось як це виглядає:
// TypeScript infers state type as never[]
const [items, setItems] = useState([]);
// So later when we try to set a value...
setItems([{ id: 1, name: 'Item 1' }]);
// Error: Argument of type '{ id: number; name: string; }[]' is not assignable
// to parameter of type 'never[]'
Причина цього: коли початкове значення вашого стану — порожній масив, TypeScript не може здогадатися, які елементи зрештою опиняться всередині нього, тому використовує тип never[] за замовчуванням.
Рішення: Завжди вказуйте явний аргумент типу, коли початковий стан — це порожній масив або null.
interface Item {
id: number;
name: string;
}
// ✅ Explicitly typed
const [items, setItems] = useState<Item[]>([]);
const [selectedItem, setSelectedItem] = useState<Item | null>(null);
У цьому фрагменті тип стану selectedItem явно визначений так, щоб приймати або об’єкт Item, або null. Кожного разу, коли ви очікуєте, що певний стан спочатку буде порожнім та заповниться пізніше, вам потрібно чітко це вказати для TypeScript — інакше виникне така помилка:
Тип 'null' не може бути присвоєний типу 'X'.
interface User {
id: number;
name: string;
email: string;
}
// TypeScript infers state as User, not User | null
const [user, setUser] = useState<User>({} as User); // Dangerous cast
// Later...
if (user.name) { /* ... */ }
// This might not catch the case where user is empty
Рішення: Використовуйте тип-уніон, який включає null, щоб TypeScript розумів, що дані ще не завантажені.
// Correctly typed, can be type User or null
const [user, setUser] = useState<User | null>(null);
// Now TypeScript forces you to handle the null case
if (user) {
console.log(user.name); // TypeScript knows user is User here
}
// Or with optional chaining:
console.log(user?.name); // string | undefined
Та сама ідея застосовується, коли ваш стан містить більш складний об’єкт. Припустимо, ви моделюєте стан форми контактів із кількома полями — правильний підхід — спочатку визначити інтерфейс, який описує цю структуру.
interface FormState {
name: string;
email: string;
message: string;
isSubmitting: boolean;
}
function SubmitMessageForm() {
const [form, setForm] = useState<FormState>({
name: '',
email: '',
message: '',
isSubmitting: false,
});
// Then do partial updates with spread operator
const handleChange = (field: keyof FormState, value: string) => {
setForm(prevState => ({ ...prevState, [field]: value }));
};
}
Проблема: Об’єкт може бути значенням 'null'.
Ця проблема постійно виникає, коли референс, створений за допомогою useRef, спочатку має значення null та призначений для посилання на елемент DOM. TypeScript попереджатиме про будь-яку спробу використання цього референса до того, як підтвердить, що він дійсно містить якусь цінність.
const inputRef = useRef<HTMLInputElement>(null);
// Typescript here knows that inputRef.current might be null
inputRef.current.focus();
// Error: Object is possibly 'null'
Рішення: перевіряйте, чи встановлено значення current, перш ніж його використовувати. Як тільки TypeScript бачить цю перевірку, воно припиняє попередження, оскільки більше не може довести, що значення може бути null.
// Null check before use
const handleFocus = () => {
if (inputRef.current) {
inputRef.current.focus();
}
};
// Or if you're more into one-liners, you can use optional chaining
inputRef.current?.focus();
Перш ніж перейти до наступної категорії, варто зазначити одну важливу зміну. React 19 змінив поведінку useRef у спосіб, який створює труднощі для багатьох розробників — а саме у розрізненні RefObject<T|null> та MutableRefObject<T>. Тепер поведінку визначає не початкове значення, яке ви передаєте, а параметр генеричного типу, який ви вказуєте цьому хуку.
До React 19 виклик useRef(null) завжди повертав об’єкт типу MutableRefObject<T>, тому поле current завжди можна було перезадати.
// This worked fine
const ref = useRef<HTMLInputElement | null>(null);
ref.current = someElement;
Починаючи з React 19, саме генеричний тип, який ви вказуєте, визначає, чи можна змінювати значення поля current.
const ref = useRef<HTMLInputElement>(null);
ref.current = someElement;
// The above will result in an error.
Тут, оскільки генеричний параметр є типом HTML-елемента, React 19 розглядає ref як той, що вказує на DOM, тож він стає лише для читання та фактично керується самим React.
Якщо ж вам потрібен ref для зберігання змінної значення замість вузла DOM, робіть так:
// Let's suppose we're building a timer
const ref = useRef<ReturnType<typeof setTimeout> | null>(null);
ref.current = setTimeout(() => {}, 1000);
Це працює тому, що тип, переданий у генеричний параметр, не є типом HTML-елемента, тож React розглядає його як звичайний змінний ref — current залишається придатним для присвоєння.
Правило для React 19: коли генеричний тип є типом HTML-елемента, current стає лише для читання та контролюється React. Коли це будь-який інший тип, current залишається придатним для запису. Коротко кажучи:
useRef<HTMLElement>(null)для вузлів DOM, якими керує React (лише для читання)
useRef<T|null>(null) для будь-якої іншої змінної величини, яку ви хочете зберігати самостійно4. Асинхронні дані та помилки API
Як тільки ви починаєте отримувати дані з API, з’являється новий набір помилок TypeScript, оскільки отримані дані спочатку мають тип unknown та повинні пройти кілька перетворень, перш ніж стати чимось, що можна безпечно використовувати.
Помилка: Тип 'unknown' не може бути присвоєний типу 'X'.
За замовчуванням результат виклику fetch має тип unknown, оскільки TypeScript не має вбудованої інформації про те, яку форму мають дані, що повертаються з певного кінцевого точки.
// Fetch response is unknown
const response = await fetch('/api/users');
const data = await response.json(); // data: any (in older TS) or unknown
// So when trying to use the fetched data:
const userName = data.name;
// With the code above, you might get a warning, something like
// 'data' is of type 'unknown'
Рішення: оголосіть явний тип для відповіді вашого API.
interface User {
id: number;
name: string;
email: string;
};
Після цього у вас зазвичай є два варіанти для застосування цього типу:
// Option 1: Type assertion. Only use this when you trust the API shape.
const response = await fetch('/api/users/1');
const data = await response.json() as User;
console.log(data.name); // You'll see that you have a string here
//////////////////////////////////////////////////////////////////////////
// Option 2: Create a generic fetch wrapper, this is a safer approach
// and it's reusable.
async function fetchJSON<T>(url: string): Promise<T> {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP error: ${response.status}`);
}
return response.json() as Promise<T>;
}
const user = await fetchJSON<User>('/api/users/1');
console.log(user.name); // TypeScript now knows this is a string
5. Дочірні елементи та помилки JSX
Компоненти в React зазвичай приймають та відображають children, а TypeScript пропонує кілька перекриваючихся типів для опису того, чим можуть бути ці дочірні елементи. Знання їхніх відмінностей допомагає уникнути цілої низки помилок типування.
Три типи часто використовуються так, ніби вони можна замінювати один на одного, хоча це не так:
- ReactNode
- ReactElement
- JSX.Element
Серед них ReactNode та ReactElement є справді різними, тоді як JSX.Element насправді є просто іншою назвою для ReactElement.
import { ReactNode, ReactElement } from 'react';
// ReactNode is the most permissive one, accepts basically
// everything React can render:
// string, number, boolean, null, undefined, ReactElement, arrays...
type ReactNode =
| ReactElement
| string
| number
| boolean
| null
| undefined
| ReactPortal
| Iterable<ReactNode>;
// ReactElement is literally a React element created by
// JSX or React.createElement
// It does not include string, number, null, undefined
type ReactElement = {
type: string | ComponentType;
props: any;
key: string | null;
};
Як загальне правило: використовуйте ReactNode для параметра children, якщо тільки у вас немає конкретної причини для його обмеження.
// Most flexible: use ReactNode for children in most cases
interface CardProps {
children: ReactNode;
};
// And use ReactElement when the requirements are not very flexible
interface StrictWrapperProps {
children: ReactElement;
};
Варто більш детально пояснити різницю між ReactNode та ReactElement.
ReactNode може бути будь-яким з наступних:
const example1 = <div>Hello</div>; // ReactElement ✅
const example2 = "Hello"; // string ✅
const example3 = 42; // number ✅
const example4 = true; // boolean ✅ (renders nothing)
const example5 = null; // null ✅ (renders nothing)
const example6 = undefined; // undefined ✅ (renders nothing)
const example7 = [<div />, <span />]; // array of ReactElements ✅
// All of the above are ReactNode
З іншого боку, ReactElement має більше обмежень:
const example1 = <div>Hello</div>;
const example2 = <Button onClick={someHandlerFn}>Submit</Button>;
const example3 = React.createElement('div', null, 'Hello');
// Under the hood, every ReactElements look like this:
{
type: 'div',
props: { children: 'Hello' },
key: null,
}
Помилка: Тип 'X' не може бути присвоєний типу 'ReactNode'.
Це зазвичай трапляється, коли ви намагаєтесь відобразити значення, яке TypeScript не вважає дійсним контентом для відображення.
// Objects are not a valid React children
interface User {
name: string;
email: string;
}
// Then if you try to do this
function DisplayUser({ user }: { user: User }) {
return <div>{user}</div>;
}
// It'll probably give you an error that looks something like...
// Error: Type 'User' is not assignable to type 'ReactNode'
// Objects are not valid React Children
Рішення: відображайте окремі поля замість всього об’єкта.
function DisplayUser({ user }: { user: User }) {
return (
<div>
<p>{user.name}</p>
<p>{user.email}</p>
</div>
);
}
Помилка: Тип елемента JSX 'X' не має жодних сигнатур конструкції чи виклику.
Спочатку це може викликати плутанину. Він з’являється тоді, коли ви передаєте значення кудись, очікуючи, що воно буде поводитися як компонент, але у TypeScript немає способу підтвердити, що воно справді таким є.
// TypeScript doesn't know 'icon' is a valid component
interface ButtonProps {
icon: object; // too vague
};
// So when you try this...
function Button({ icon: Icon }: ButtonProps) {
return <Icon />;
}
// The above code will give you the error
// Error: JSX element type 'Icon' does not have any construct
// or call signatures
Рішення: задайте тип динамічних компонентів за допомогою React.ComponentType:
import { ComponentType } from 'react';
interface ButtonProps {
icon: ComponentType;
};
function Button({ icon: Icon }: ButtonProps) {
return (
<button>
<Icon />
</button>
);
}
// TypeScript now knows Icon is a valid component
Якщо цей компонент також приймає пропси, ви можете описати їх явно:
interface IconProps {
size?: number;
color?: string;
};
interface ButtonProps {
icon: ComponentType<IconProps>;
};
function Button({ icon: Icon }: ButtonProps) {
return (
<button>
<Icon size={12} color="white" />
</button>
);
}
// So now the props are typed too
Корисним скороченням, яке варто знати, є вбудований у React тип-функція PropsWithChildren. Він автоматично додає children?: ReactNode до будь-якого інтерфейсу пропсів, навколо якого він розміщується, що дозволяє уникнути повторного оголошення цього поля щоразу:
import { PropsWithChildren } from 'react';
// Now instead of doing this
interface CardProps {
title: string;
children?: ReactNode;
};
// You can do this
type CardProps = PropsWithChildren<{ title: string }>;
// So with this, instead of explicitly typing children manually,
// you can use the above code
function Card({ title, children }: CardProps) {
return (
<div>
<h1>{title}</h1>
<div>{children}</div>
</div>
);
}
Проблема: відображення списків елементів
TypeScript накладає суворі правила щодо ключів у відображених списках, але більшість типових помилок, з якими ви тут зіткнетеся, насправді походять від того, як самі елементи описані за типом, а не від ключів.
// Items might be undefined, or item.id might not be a valid key type
function ItemList({ items }: { items: Item[] | undefined }) {
return (
<ul>
{
items.map(item => (
<li key={item.id}>{item.name}</li>
))
}
</ul>
);
}
// Error: Object is possibly 'undefined'
Рішення: захищайтеся від значень undefined та переконуйтесь, що типи правильно відповідають один одному:
function ItemList({ items = [] }: { items?: Item[] }) {
return (
<ul>
{
items.map((item) => (
<li key={item.id}>{item.name}</li>
))
}
</ul>
);
}
Щоб завершити…
Помилки TypeScript перестають здаватися перешкодами, як тільки ви починаєте сприймати їх як підказки. Ця зміна відбувається у момент, коли ви перестаєте реагувати з фразою „Ух, знову це“ та починаєте запитувати: „Що насправді намагається сказати мені TypeScript?“
Помилки, про які йдеться в цьому посібнику, — це ті, з якими ви будете стикатися протягом усього робочого процесу з React та TypeScript. Те, що відрізняє розробника, який постійно бореться з системою типів, від того, хто комфортно з нею працює, зазвичай зводиться лише до розпізнавання шаблонів, більше нічого. На цьому етапі ви вже знаєте ці шаблони.