React + TypeScript: необхідний мінімум
Чимало React-розробників запитують себе: чи треба мені вчити TypeScript? Ще як треба!
Переваги вивчення TS можуть бути зведені до наступного:
- ваші шанси отримати більш високооплачувану роботу збільшаться;
- у вашому коді буде набагато менше багів, його буде легше читати та підтримувати;
- рефакторити код та оновлювати залежності стане набагато простіше.
Ця стаття являє собою мінімальне введення з використання TypeScript у React.
Основи TypeScript, необхідні React
Примітиви
Існує 3 основні примітиви, які є фундаментом для інших типів:
string // наприклад, "Pat" boolean // наприклад, true number // наприклад, 23 чи 1.99
Масиви
Тип масиву складається з примітивів чи інших типів:
number[] // наприклад, [1, 2, 3] string[] // наприклад, ["Lisa", "Pat"] User[] // кастомний тип, наприклад, [{ name: "Pat" }, { name: "Lisa" }]
Об’єкти
Об’єкти усюди. Приклад простого об’єкта:
const user = {
firstName: "Pat",
age: 23,
isNice: false,
role: "CTO",
skills: ["HTML", "CSS", "jQuery"]
}
Тип, що описує цей об’єкт, виглядає так:
type User = {
firstName: string;
age: number;
isNice: boolean;
role: string;
skills: string[];
}
Припустимо, що користувач має друзів, які також є користувачами:
type User = {
// ...
friends: User[];
}
Пет завжди ставив кар’єру на перше місце, тому в нього цілком може не бути друзів:
const user = {
firstName: "Pat",
age: 23,
isNice: false,
role: "CTO",
skills: ["CSS", "HTML", "jQuery"],
friends: undefined,
}
У TypeScript для позначення опціонального (необов’язкового) поля використовується символ ?
після назви поля:
type User = {
// ...
friends?: User[];
}
Перерахування
Ми визначили тип поля role
як string
:
type User = {
// ...
role: string;
}
Пету це не подобається. Він вважає, що тип string
недостатньо суворий. Його працівники мають вибирати з обмеженого набору ролей.
Для цього відмінно підійде перерахування (enumeration, enum):
enum UserRole {
CEO,
CTO,
SUBORDINATE,
}
Так набагато краще. Але Пет знає, що значення елементів перерахування є числа. Значенням CEO
є 0
, CTO
– 1
, а SUBORDINATE
– 2
. Пету це не до вподоби.
На щастя, як значення елементів перерахування можна використовувати рядки:
enum UserRole {
CEO = "ceo",
CTO = "cto",
SUBORDINATE = "inferior-person",
}
Тепер Пет задоволений:
enum UserRole {
CEO = "ceo",
CTO = "cto",
SUBORDINATE = "inferior-person",
}
type User = {
firstName: string;
age: number;
isNice: boolean;
role: UserRole;
skills: string[];
friends?: User[];
}
const user = {
firstName: "Pat",
age: 23,
isNice: false,
role: UserRole.CTO, // равняется "cto"
skills: ["HTML", "CSS", "jQuery"],
}
Функції
Пет любить показувати свою владу, звільняючи підлеглих. Визначимо функцію, що дозволяє Пету робити це.
Типізація параметрів функції
Існує, як мінімум, 3 способи ідентифікувати звільнену особу. По-перше, ми можемо використовувати декілька параметрів:
function fireUser(firstName: string, age: number, isNice: boolean) {
// ...
}
// чи так
const fireUser = (firstName: string, age: number, isNice: boolean) => {
// ...
}
По-друге, ми можемо обернути параметри в об’єкт і визначити типи в об’єкті:
function fireUser({ firstName, age, isNice }: {
firstName: string;
age: number;
isNice: boolean;
}) {
// ...
}
Нарешті ми можемо винести параметри в окремий тип. Спойлер: така техніка часто використовується для проповнення компонентів React:
type User = {
firstName: string;
age: number;
role: UserRole;
}
function fireUser({ firstName, age, role }: User) {
// ...
}
Типізація значення, що повертається функцією
Пет вважає, що просто звільнити працівника недостатньо. Тому може бути гарною ідеєю повертати користувача з функції.
Для визначення типу значення, що повертається функцією, також є кілька способів. Ми можемо додати : Type
після закривання дужки списку параметрів функції:
function fireUser(firstName: string, age: number, role: UserRole): User {
// ...
return { firstName, age, role };
}
// чи так
const fireUser = (firstName: string, age: number, role: UserRole): User => {
// ...
return { firstName, age, role };
}
Якщо, наприклад, ми спробуємо повернути null
отримаємо помилку:
Насправді, найчастіше ми не маємо необхідності визначати тип значення, що повертається функцією, явно. TypeScript відмінно справляється з припущенням (висновком) таких типів:
За наявності сумнівів у коректності TypeScript типу, що виводиться, достатньо навести курсор на назву змінної або функції (спасибі сучасним редакторам коду).
Інші речі, з якими вам доведеться мати справу
Існує ще кілька корисних речей, які можуть знадобитися при використанні TypeScript у React. Мабуть, найважливішими з них є:
React і TypeScript
Коли мова заходить про використання TypeScript в React, слід пам’ятати, що компоненти і хуки React — це лише функції, а props — лише об’єкти.
Функціональні компоненти
У більшості випадків у нас немає необхідності визначати тип, який повертається компонентом:
function UserProfile() {
return <div>If you're Pat: YOU'RE AWESOME!!</div>
}
Типом, що повертається компонентом є JSX.Element
, як видно на наведеному нижче зображенні:
Якщо ми спробуємо повернути з компонента не JSX
, то отримаємо попередження:
В даному випадку об’єкт user
не є валідним JSX
:
'UserProfile' cannot be used as a JSX component.
Its return type 'User' is not a valid JSX element.
Props
Як зазначалося раніше, props — це лише об’єкти:
enum UserRole {
CEO = "ceo",
CTO = "cto",
SUBORDINATE = "inferior-person",
}
type UserProfileProps = {
firstName: string;
role: UserRole;
}
function UserProfile({ firstName, role }: UserProfileProps) {
if (role === UserRole.CTO) {
return <div>Hey Pat, you're AWESOME!!</div>
}
return <div>Hi {firstName}, you suck!</div>
}
// чи так
const UserProfile = ({ firstName, role }: UserProfileProps) => {
if (role === UserRole.CTO) {
return <div>Hey Pat, you're AWESOME!!</div>
}
return <div>Hi {firstName}, you suck!</div>
}
Зверніть увагу : при роботі над React-проектами ви, швидше за все, зустрінете багато коду, в якому використовується тип React.FC
або React.FunctionComponent
:
const UserProfile: React.FC<UserProfileProps>({ firstName, role }) {
// ...
}
Використовувати ці типи більше не рекомендується .
Props-коллбеки
Як пропов компонентам часто передаються функції зворотного виклику (коллбеки):
type UserProfileProps = {
id: string;
firstName: string;
role: UserRole;
fireUser: (id: string) => void;
};
function UserProfile({ id, firstName, role, fireUser }: UserProfileProps) {
if (role === UserRole.CTO) {
return <div>Hey Pat, you're AWESOME!!</div>;
}
return (
<>
<div>Hi {firstName}, you suck!</div>
<button onClick={() => fireUser(id)}>Fire this loser!</button>
</>
);
}
void
означає, що функція нічого не повертає.
Дефолтні props
Як ви пам’ятаєте, ми можемо зробити поле опціональним за допомогою ?
. Те саме можна робити з props:
type UserProfileProps = {
age: number;
role?: UserRole;
}
Опціональний props може мати значення за замовчуванням:
function UserProfile({ firstName, role = UserRole.SUBORDINATE }: UserProfileProps) {
if (role === UserRole.CTO) {
return <div>Hey Pat, you're AWESOME!!</div>
}
return <div>Hi {firstName}, you suck!</div>
}
Хуки
useState()
useState
– Найпопулярніший хук React. У багатьох випадках його не слід типізувати. TypeScript здатний вивести типи значень, що повертаються useState()
на основі початкового значення:
function UserProfile({ firstName, role }: UserProfileProps) {
const [isFired, setIsFired] = useState(false);
return (
<>
<div>Hi {firstName}, you suck!</div>
<button onClick={() => setIsFired(!isFired)}>
{isFired ? "Oops, hire them back!" : "Fire this loser!"}
</button>
</>
);
}
Тепер ми в безпеці. При спробі оновити стан не логічним значенням отримуємо помилку:
У деяких випадках TypeSctipt не може вивести тип значень, що повертаються useState()
:
// TS не знає, элементи якого типу будуть у масиву
const [names, setNames] = useState([]);
// початковим значенням є `undefined`, тому TypeScript-у невідом справжній тип
const [user, setUser] = useState();
// теж саме справедливо для `null` в якості початкового значення
const user = useState(null);
useState()
реалізовано за допомогою загального типу (дженерика, generic type). Ми можемо використовувати це для типізації стану:
// типом `names` є `string[]`
const [names, setNames] = useState<string[]>([]);
setNames(["Pat", "Lisa"]);
// типом user є `User | undefined`
const [user, setUser] = useState<User>();
setUser({ firstName: "Pat", age: 23, role: UserRole.CTO });
setUser(undefined);
// типом `user` є `User | null`
const [user, setUser] = useState<User | null>(null);
setUser({ firstName: "Pat", age: 23, role: UserRole.CTO });
setUser(null);
Ми не будемо розглядати інші вбудовані хуки, вони типизуються схожим чином, за винятком того useEffect()
, що не потребує типізації.
Хуки на замовлення
Кастомний хук – це просто функція:
function useFireUser(firstName: string) {
const [isFired, setIsFired] = useState(false);
const hireAndFire = () => setIsFired(!isFired);
return {
text: isFired ? `Oops, hire ${firstName} back!` : "Fire this loser!",
hireAndFire,
};
}
function UserProfile({ firstName, role }: UserProfileProps) {
const { text, hireAndFire } = useFireUser(firstName);
return (
<>
<div>Hi {firstName}, you suck!</div>
<button onClick={hireAndFire}>
{text}
</button>
</>
);
}
Події
З вбудованими обробниками подій у React працювати легко, оскільки TypeScript-у відомі типи подій:
function FireButton() {
return (
<button onClick={(event) => event.preventDefault()}>
Fire this loser!
</button>
);
}
Але визначення оброблювача у вигляді окремої функції докорінно змінює справу:
function FireButton() {
const onClick = (event: /* ??? */) => {
event.preventDefault();
};
return (
<button onClick={onClick}>
Fire this loser!
</button>
);
}
Який тип має event
? Існує 2 підходи:
- гуглити (не рекомендується, викликає запаморочення));
- приступити до реалізації вбудованої функції та дозволити TS вивести типи:
Щасливі копіпастінг. Нам навіть не потрібно розуміти, що відбувається (більшість обробників є дженериками, як useState()
).
function FireButton() {
const onClick = (event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
event.preventDefault();
};
return (
<button onClick={onClick}>
Fire this loser!
</button>
);
}
Що щодо оброблювача зміни значення інпуту?
function Input() {
const onChange = (event: React.ChangeEvent<HTMLInputElement>) => {
console.log(event.target.value);
};
return <input onChange={onChange} />;
}
А команда?
function Select() {
const onChange = (event: React.ChangeEvent<HTMLSelectElement>) => {
console.log(event.target.value);
};
return <select onChange={onChange}>...</select>;
}
Дочірні компоненти
Композиція компонентів, від якої ми всі в захваті, передбачає передачу пропа children
:
type LayoutProps = {
children: React.ReactNode;
};
function Layout({ children }: LayoutProps) {
return <div>{children}</div>;
}
Тип React.ReactNode
надає більшу свободу вибору переданого значення. Він дозволяє передавати майже будь-що (крім об’єкта):
Якщо як props повиннa передаватися тільки розмітка, тип children
можна обмежити до React.ReactElement
або JSX.Element
(що по суті те саме):
type LayoutProps = {
children: React.ReactElement; // чи `JSX.Element`
};
Ці типи є набагато суворішими:
Сторонні бібліотеки
Додавання типів
Сьогодні багато сторонніх бібліотек містять відповідні типи. У цьому випадку окремий пакет (з типами) не потрібно встановлювати.
Типи для великої кількості існуючих бібліотек містяться в репозиторії DefinitelyTyped на GitHub і публікуються під егідою організації @types
(включаючи типи React). При встановленні пакета без типів та його імпорті отримуємо таку помилку:
Копіюємо виділену команду та виконуємо її в терміналі:
npm i --save-dev @types/styled-components
Якщо ви все ж таки зіткнулися з відсутністю типів для сторонньої бібліотеки, доведеться визначити глобальні типи самостійно у файлі .d.ts
(ми не розглядатимемо його в рамках статті).
Використання дженериків
Бібліотеки розраховані на різні випадки використання, тому вони мають бути гнучкими. Для забезпечення гнучкості типів використовуються дженерики. Ми їх уже бачили в useState()
:
const [names, setNames] = useState<string[]>([]);
Такий прийом дуже поширеним для сторонніх бібліотек. Приклад з Axios:
import axios from "axios"
async function fetchUser() {
const response = await axios.get<User>("https://example.com/api/user");
return response.data;
}
Реагувати на запит:
import { useQuery } from "@tanstack/react-query";
function UserProfile() {
// загальні типи даних і помилки
const { data, error } = useQuery<User, Error>(["user"], () => fetchUser());
if (error) {
return <div>Error: {error.message}</div>;
}
// ...
}
Стилізовані компоненти:
import styled from "styled-components";
// загальний тип для props
const MenuItem = styled.li<{ isActive: boolean }>`
background: ${(props) => (props.isActive ? "red" : "gray")};
`;
function Menu() {
return (
<ul>
<MenuItem isActive>Menu Item 1</MenuItem>
</ul>
);
}
Способи усунення несправностей
Початок роботи з React & TypeScript
Створення нового React-проекту з підтримкою TypeScript – справа однієї команди. Я рекомендую використовувати шаблони Vite+React+TS або Next.js+TS:
npm create vite [project-name] --template react-ts
npx create-next-app [project-name] --ts
Виявлення правильного типу
Ми це вже розглядали, але повторимо ще раз: починаємо писати вбудовану функцію та дозволяємо TypeScript вивести правильний тип події:
За наявності сумнівів щодо кількості доступних параметрів набираємо (...args) =>
та отримуємо відповідний масив:
Вивчення типу
Найпростіший спосіб отримати список всіх доступних полів типу – використовувати автозавершення в IDE. Для цього достатньо натиснути CTRL + Пробіл (Windows) або Option + Пробіл (Mac):
Щоб перейти до визначення типу, слід натиснути CTRL + Click (Windows) або CMD + Click (Mac):
Читання повідомлень про помилки
Повідомлення про помилки та попередження TS є дуже інформативними, головне – навчитися їх правильно читати. Розглянемо приклад:
function Input() {
return <input />;
}
function Form() {
return (
<form>
<Input onChange={() => console.log("change")} />
</form>
);
}
Ось що показує TS:
Що це означає? Що ще за тип IntrinsicAttributes
? При роботі з бібліотеками (у тому числі з самим React) ви часто зустрічатимете дивні назви типів, на кшталт цього.
Моя порада: ігноруйте їх спочатку.
Найважливішою частиною є останній рядок:
Property 'onChange' does not exist on type...
Дивимося на визначення компонента Input
:
function Input() {
return <input />;
}
У нього немає пропа onChange
. Саме це “не подобається” TypeScript.
Тепер розглянемо складніший приклад:
const MenuItem = styled.li`
background: "red";
`;
function Menu() {
return <MenuItem isActive>Menu Item</MenuItem>;
}
Нічого собі повідомлення про помилку! Без паніки: прокручуємо в кінець повідомлення – як правило, відповідь знаходиться там:
Перетини
Припустимо, що у нас є тип User
, визначений в окремому файлі, наприклад types.ts
:
export type User = {
firstName: string;
role: UserRole;
}
Він використовується для типізації пропов компонента UserProfile
:
function UserProfile({ firstName, role }: User) {
// ...
}
Який використовується в компоненті UserPage
:
function UserPage() {
const user = useFetchUser();
return <UserProfile {...user} />;
}
Поки все добре. Але що якщо UserProfile
буде приймати ще один проп – функцію fireUser
?
function UserProfile({ firstName, role, fireUser }: User) {
return (
<>
<div>Hi {firstName}, you suck!</div>
<button onClick={() => fireUser({ firstName, role })}>
Fire this loser!
</button>
</>
);
}
Отримуємо помилку:
Цю проблему можна вирішити за допомогою перетину (intersection type). При перетині всі поля двох типів поєднуються в один тип. Перетини створюються за допомогою символу &
:
type User = {
firstName: string;
role: UserRole;
}
// `UserProfileProps` буде вміщувати в себе всі поля `User` та `fireUser`
type UserProfileProps = User & {
fireUser: (user: User) => void;
}
function UserProfile({ firstName, role, fireUser }: UserProfileProps) {
return (
<>
<div>Hi {firstName}, you suck!</div>
<button onClick={() => fireUser({ firstName, role })}>
Fire this loser!
</button>
</>
);
}
Більш “чистим” способом є визначення окремих типів для компонентів, що приймаються props:
type User = {
firstName: string;
role: UserRole;
}
// !
type UserProfileProps = {
user: User;
fireUser: (user: User) => void;
}
function UserProfile({ user, onClick }: UserProfileProps) {
return (
<>
<div>Hi {user.firstName}, you suck!</div>
<button onClick={() => fireUser(user)}>
Fire this loser!
</button>
</>
);
}
Дякую за увагу та happy coding!
Переклад статті “Minimal TypeScript Crash Course For React“