Пошук уроків, статей та іншого контенту
Розберете поширені помилки TypeScript у React та способи їх діагностики й виправлення.
TypeScript перевіряє типи ще до запуску застосунку. У React це особливо корисно для:
props компонентів;
стану та значень, які повертають хуки;
обробників подій;
ref;
асинхронних даних;
children.
Повідомлення TypeScript часто виглядає складніше, ніж сама причина помилки. Найкращий підхід — читати його від кінця до початку:
Знайти рядок коду, на який указує повідомлення.
Визначити фактичний тип значення.
Порівняти його з типом, якого очікує React або ваш компонент.
Виправити джерело невідповідності, а не приховувати помилку через any.
Якщо prop описаний як обов’язковий, його потрібно передати під час використання компонента.
type UserCardProps = {
name: string;
age: number;
};
function UserCard({ name, age }: UserCardProps) {
return (
<article>
<h2>{name}</h2>
<p>Вік: {age}</p>
</article>
);
}
export default function App() {
return <UserCard name="Олена" age={28} />;
}Якщо передати лише name:
<UserCard name="Олена" />TypeScript повідомить, що властивість age відсутня.
Є два правильні варіанти:
передати prop;
зробити prop необов’язковим, якщо компонент уміє працювати без нього.
type UserCardProps = {
name: string;
age?: number;
};
function UserCard({ name, age }: UserCardProps) {
return (
<article>
<h2>{name}</h2>
{age !== undefined && <p>Вік: {age}</p>}
</article>
);
}Знак ? означає, що значення може бути відсутнім. У такому разі його тип фактично стає number | undefined, тому перед використанням потрібно перевірити age.
React не перетворює типи автоматично з погляду TypeScript. Якщо prop має тип number, рядок передавати не можна:
type ProductProps = {
price: number;
};
function Product({ price }: ProductProps) {
return <p>{price} грн</p>;
}
// Помилка: рядок не є number
<Product price="100" />;Правильний варіант:
<Product price={100} />Якщо значення приходить із текстового поля або URL, воно часто є рядком. Його потрібно явно перетворити:
const priceFromInput = "100";
const price = Number(priceFromInput);
<Product price={price} />;Перед перетворенням варто враховувати, що Number() може повернути NaN. Для даних від користувача бажано виконувати додаткову перевірку.
useStateTypeScript виводить тип стану з початкового значення. Це може призвести до несподіваних обмежень.
const [items, setItems] = useState([]);У багатьох випадках порожній масив буде виведений як масив типу never[]. Після цього додавання об’єкта може спричинити помилку.
Правильно явно описати тип елемента:
type Task = {
id: number;
title: string;
completed: boolean;
};
const [tasks, setTasks] = useState<Task[]>([]);Тепер TypeScript знає структуру кожного елемента масиву:
setTasks((currentTasks) => [
...currentTasks,
{
id: 1,
title: "Вивчити TypeScript",
completed: false,
},
]);Якщо стан спочатку не має значення, його тип потрібно описати через об’єднання з null або undefined.
type User = {
id: number;
name: string;
};
const [user, setUser] = useState<User | null>(null);Не можна одразу звертатися до user.name, оскільки на початку user дорівнює null:
// Помилка: user може бути null
user.name;Потрібна перевірка:
if (user) {
console.log(user.name);
}У JSX можна використати умовний рендеринг:
return <div>{user ? <p>{user.name}</p> : <p>Завантаження...</p>}</div>;Не варто без потреби використовувати оператор !:
user!.name;Він змушує TypeScript повірити, що user точно існує, але не змінює реальну поведінку програми. Якщо значення все ще null, під час виконання виникне помилка.
Поширена помилка — використовувати загальний або неправильний тип події.
Для звичайного текстового поля використовуйте React.ChangeEvent<HTMLInputElement>:
import { useState } from "react";
export default function SearchInput() {
const [query, setQuery] = useState("");
function handleChange(event: React.ChangeEvent<HTMLInputElement>) {
setQuery(event.target.value);
}
return (
<label>
Пошук
<input value={query} onChange={handleChange} />
</label>
);
}Тип елемента має відповідати реальному елементу:
HTMLInputElement — для <input>;
HTMLTextAreaElement — для <textarea>;
HTMLSelectElement — для <select>.
Наприклад:
function handleDescriptionChange(
event: React.ChangeEvent<HTMLTextAreaElement>,
) {
console.log(event.target.value);
}Для обробника натискання кнопки використовуйте React.MouseEvent<HTMLButtonElement>:
function handleClick(event: React.MouseEvent<HTMLButtonElement>) {
event.currentTarget.disabled = true;
}Якщо параметр події не використовується, його можна не оголошувати:
function handleClick() {
console.log("Кнопку натиснуто");
}React сам передасть подію обробнику, але функція без параметрів залишається коректною.
Не викликайте функцію під час рендерингу, якщо потрібно передати її як обробник:
// Неправильно: функція викликається одразу
<button onClick={handleDelete(id)}>Видалити</button>Правильно передати функцію, яка буде викликана після події:
<button onClick={() => handleDelete(id)}>Видалити</button>Якщо функція не приймає аргументів, можна передати її без дужок:
<button onClick={handleSave}>Зберегти</button>childrenchildren — це вміст між відкривальним і закривальним тегами компонента. Його тип може бути різним: текст, JSX-елементи, фрагмент або масив елементів.
Зручно використовувати React.ReactNode:
import type { ReactNode } from "react";
type PanelProps = {
title: string;
children: ReactNode;
};
function Panel({ title, children }: PanelProps) {
return (
<section>
<h2>{title}</h2>
<div>{children}</div>
</section>
);
}
export default function App() {
return (
<Panel title="Налаштування">
<p>Вміст панелі</p>
</Panel>
);
}Якщо children може бути відсутнім, зробіть його необов’язковим:
type PanelProps = {
title: string;
children?: ReactNode;
};Не слід типізувати children лише як JSX.Element, якщо компонент має підтримувати текст або кілька дочірніх елементів.
refuseRef для DOM-елементаДля посилання на DOM-елемент потрібно вказати тип елемента та початкове значення null:
import { useEffect, useRef } from "react";
export default function SearchForm() {
const inputRef = useRef<HTMLInputElement>(null);
useEffect(() => {
inputRef.current?.focus();
}, []);
return <input ref={inputRef} placeholder="Введіть запит" />;
}inputRef.current має тип HTMLInputElement | null, тому TypeScript вимагає перевірку.
Безпечні способи звернення:
inputRef.current?.focus();або:
if (inputRef.current) {
inputRef.current.focus();
}Помилкою буде передати ref без типу або спробувати використати властивість, якої немає в конкретного елемента:
const inputRef = useRef<HTMLInputElement>(null);
// Помилка: у HTMLInputElement немає методу play
inputRef.current?.play();Дані з API часто перебувають у трьох станах:
запит ще виконується;
дані успішно отримано;
сталася помилка.
Ці стани потрібно врахувати в типах і JSX.
import { useEffect, useState } from "react";
type Todo = {
id: number;
title: string;
completed: boolean;
};
export default function TodoList() {
const [todos, setTodos] = useState<Todo[]>([]);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
async function loadTodos() {
try {
const response = await fetch(
"https://jsonplaceholder.typicode.com/todos?_limit=5",
);
if (!response.ok) {
throw new Error("Не вдалося завантажити завдання");
}
const data: Todo[] = await response.json();
setTodos(data);
} catch (requestError) {
setError(
requestError instanceof Error
? requestError.message
: "Невідома помилка",
);
} finally {
setIsLoading(false);
}
}
loadTodos();
}, []);
if (isLoading) {
return <p>Завантаження...</p>;
}
if (error) {
return <p role="alert">{error}</p>;
}
return (
<ul>
{todos.map((todo) => (
<li key={todo.id}>
{todo.title} — {todo.completed ? "виконано" : "не виконано"}
</li>
))}
</ul>
);
}Важливі деталі:
useState<Todo[]>([]) описує масив отриманих об’єктів;
error має тип string | null;
response.json() за замовчуванням не гарантує структуру даних;
тип Todo[] допомагає перевіряти використання даних після отримання.
Оголошення типу для response.json() не перевіряє сервер під час виконання. Воно лише повідомляє TypeScript, як ви очікуєте вигляд даних. Якщо API повертає іншу структуру, потрібна окрема перевірка даних.
Якщо масив містить один тип даних, callback для map має працювати саме з цим типом:
type Message = {
id: number;
text: string;
};
const messages: Message[] = [
{ id: 1, text: "Привіт" },
{ id: 2, text: "Як справи?" },
];
const messageElements = messages.map((message) => (
<li key={message.id}>{message.text}</li>
));TypeScript знає, що message.id є числом, а message.text — рядком.
keyReact очікує стабільний key під час рендерингу списку:
{messages.map((message) => (
<li key={message.id}>{message.text}</li>
))}key не потрібно додавати до типу Message, якщо він використовується лише для React. Це спеціальний атрибут JSX, а не звичайний prop, доступний усередині компонента.
Якщо компоненту справді потрібен ідентифікатор, передайте його окремим prop:
type MessageItemProps = {
messageId: number;
text: string;
};Property does not existТаке повідомлення означає, що TypeScript не знаходить указану властивість у типі.
type Profile = {
name: string;
};
function ProfileView({ profile }: { profile: Profile }) {
// Помилка: властивість email не описана в Profile
return <p>{profile.email}</p>;
}Є два можливі виправлення:
використовувати правильну властивість;
додати email до типу, якщо вона справді є частиною даних.
type Profile = {
name: string;
email: string;
};Не слід додавати властивість до типу лише для того, щоб прибрати повідомлення. Тип має відповідати реальній моделі даних.
Type X is not assignable to type YЦе загальне повідомлення про несумісність типів.
Наприклад:
type Status = "idle" | "loading" | "success" | "error";
const [status, setStatus] = useState<Status>("idle");
// Помилка: "done" не входить до Status
setStatus("done");Виправлення залежить від логіки застосунку:
setStatus("success");або потрібно додати новий допустимий стан:
type Status = "idle" | "loading" | "success" | "error" | "done";Об’єднання літеральних типів корисне для значень із фіксованим набором варіантів. Воно не дозволяє випадково передати довільний рядок.
Під час діагностики рухайтеся від місця використання до джерела значення.
Наведіть курсор на змінну, prop або функцію. Редактор покаже тип, який вивів TypeScript.
Особливо корисно перевіряти:
тип елемента масиву;
тип event.target;
тип ref.current;
тип початкового значення стану;
тип значення, яке повертає функція.
Повідомлення може містити кілька рівнів:
Type 'string' is not assignable to type 'number'.Це означає:
фактичне значення має тип string;
у цьому місці очікується number.
Потрібно не просто поставити приведення типу, а з’ясувати, чому значення стало рядком.
Якщо значення має об’єднаний тип, перевірка допомагає TypeScript визначити конкретний варіант:
function formatValue(value: string | number) {
if (typeof value === "number") {
return value.toFixed(2);
}
return value.toUpperCase();
}Після перевірки TypeScript знає, які операції дозволені для кожного типу.
Зміна типу prop може вимагати оновлення:
самого компонента;
місця його використання;
початкового значення стану;
callback-функцій;
даних, які передаються з API.
Типи в React-компонентах утворюють ланцюжок. Виправлення лише одного фрагмента часто відкриває наступну реальну помилку.
У наступному прикладі разом використано типізовані props, стан, подію форми та масив об’єктів:
import { FormEvent, useState } from "react";
type Task = {
id: number;
title: string;
};
type TaskFormProps = {
onAdd: (task: Task) => void;
};
function TaskForm({ onAdd }: TaskFormProps) {
const [title, setTitle] = useState("");
function handleSubmit(event: FormEvent<HTMLFormElement>) {
event.preventDefault();
const trimmedTitle = title.trim();
if (!trimmedTitle) {
return;
}
onAdd({
id: Date.now(),
title: trimmedTitle,
});
setTitle("");
}
return (
<form onSubmit={handleSubmit}>
<input
value={title}
onChange={(event) => setTitle(event.target.value)}
placeholder="Нова задача"
/>
<button type="submit">Додати</button>
</form>
);
}
export default function TaskList() {
const [tasks, setTasks] = useState<Task[]>([]);
function addTask(task: Task) {
setTasks((currentTasks) => [...currentTasks, task]);
}
return (
<main>
<h1>Задачі</h1>
<TaskForm onAdd={addTask} />
<ul>
{tasks.map((task) => (
<li key={task.id}>{task.title}</li>
))}
</ul>
</main>
);
}Тут TypeScript перевіряє:
що TaskForm отримує prop onAdd;
що onAdd приймає об’єкт типу Task;
що handleSubmit працює з формою;
що стан містить лише масив Task;
що кожен елемент має id і title.
anyfunction UserCard({ user }: { user: any }) {
return <p>{user.name}</p>;
}any вимикає перевірку типів для конкретного значення. Через це помилки можуть проявитися лише під час виконання.
Краще описати форму даних:
type User = {
name: string;
email: string;
};
function UserCard({ user }: { user: User }) {
return (
<article>
<p>{user.name}</p>
<p>{user.email}</p>
</article>
);
}const user = responseData as User;Приведення as User не перетворює об’єкт і не перевіряє його вміст. Воно лише змінює припущення TypeScript.
Використовуйте приведення лише тоді, коли ви справді маєте підстави довіряти типу. Для невідомих зовнішніх даних краще виконувати перевірку структури.
// Неправильно для input
function handleChange(event: React.MouseEvent<HTMLButtonElement>) {
console.log(event.currentTarget.value);
}Тип події повинен відповідати елементу та виду події:
function handleChange(event: React.ChangeEvent<HTMLInputElement>) {
console.log(event.currentTarget.value);
}nullconst [selectedId, setSelectedId] = useState<number | null>(null);
// Помилка: selectedId може бути null
const result = selectedId.toFixed(0);Спочатку перевірте значення:
if (selectedId !== null) {
const result = selectedId.toFixed(0);
}keyitems.map((item, index) => <li key={index}>{item.name}</li>);Це не завжди помилка TypeScript, але може спричинити проблеми React під час зміни або сортування списку. Якщо у даних є стабільний ідентифікатор, використовуйте його:
items.map((item) => <li key={item.id}>{item.name}</li>);Типізуйте props окремими типами або інтерфейсами.
Для порожніх масивів у useState явно вказуйте тип елементів.
Використовуйте null або undefined у типі стану, якщо значення може бути відсутнім.
Тип події має відповідати HTML-елементу та дії.
Для children зазвичай підходить ReactNode.
Для DOM-референсів використовуйте useRef<ElementType>(null) і перевіряйте current.
Описуйте форму асинхронних даних та враховуйте стани завантаження й помилки.
Читайте повідомлення TypeScript як порівняння фактичного та очікуваного типів.
Не приховуйте помилки через any або безпідставне as.
Звужуйте об’єднані типи перевірками перед використанням значень.