Привет, друзья!
Представляю вашему вниманию перевод этой замечательной статьи.
Многие React-разработчики спрашивают себя: надо ли мне учить TypeScript? Еще как надо!
Преимущества изучения TS могут быть сведены к следующему:
- ваши шансы получить более высокооплачиваемую работу сильно увеличатся;
- в вашем коде будет намного меньше багов, его будет легче читать и поддерживать;
- рефакторить код и обновлять зависимости станет гораздо проще.
Эта статья представляет собой минимальное введение по использованию TS в React.
Антигероем нашей истории будет Пэт — очень неприятный технический директор.
Основы TS, необходимые в 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,
}
В TS для обозначения опционального (необязательного) поля используется символ ?
после названия поля:
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
, например, то получим ошибку:
На самом деле, чаще всего у нас нет необходимости определять тип значения, возвращаемого функцией, явно. TS отлично справляется с предположением (выводом) таких типов:
При наличии сомнений в корректности выводимого TS типа, достаточно навести курсор на название переменной или функции (спасибо современным редакторам кода).
Другие вещи, с которыми вам придется иметь дело
Существует еще несколько полезных вещей, которые могут вам пригодиться при использовании TS в React. Пожалуй, самыми важными из них являются:
React и TS
Когда речь заходит об использовании TS в React, следует помнить, что компоненты и хуки React — это всего лишь функции, а пропы — всего лишь объекты.
Функциональные компоненты
В большинстве случаев у нас нет необходимости определять тип, возвращаемый компонентом:
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.
Пропы
Как отмечалось ранее, пропы — это всего лишь объекты:
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 }) {
// ...
}
Использовать эти типы больше не рекомендуется.
Пропы-коллбэки
В качестве пропов компонентам часто передаются функции обратного вызова (коллбэки):
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
означает, что функция ничего не возвращает.
Дефолтные пропы
Как вы помните, мы можем сделать поле опциональным с помощью ?
. Тоже самое можно делать с пропами:
type UserProfileProps = {
age: number;
role?: UserRole;
}
Опциональный проп может иметь значение по умолчанию:
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. Во многих случаях его не нужно типизировать. TS способен вывести типы значений, возвращаемых 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>
</>
);
}
Теперь мы в безопасности. При попытке обновить состояние не логическим значением получаем ошибку:
В некоторых случаях TS не может вывести тип значений, возвращаемых useState()
:
// TS не знает, элементы какого типа будет содержать массив
const [names, setNames] = useState([]);
// начальным значением является `undefined`, поэтому TS неизвестен настоящий тип
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 работать легко, поскольку TS известны типы событий:
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
предоставляет большую свободу выбора передаваемого значения. Он позволяет передавать почти что угодно (кроме объекта):
Если в качестве пропа должна передаваться только разметка, тип 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;
}
React Query:
import { useQuery } from "@tanstack/react-query";
function UserProfile() {
// общие типы данных и ошибки
const { data, error } = useQuery<User, Error>(["user"], () => fetchUser());
if (error) {
return <div>Error: {error.message}</div>;
}
// ...
}
Styled Components:
import styled from "styled-components";
// общий тип для пропов
const MenuItem = styled.li<{ isActive: boolean }>`
background: ${(props) => (props.isActive ? "red" : "gray")};
`;
function Menu() {
return (
<ul>
<MenuItem isActive>Menu Item 1</MenuItem>
</ul>
);
}
Способы устранения неполадок
Начало работы с React & TS
Создание нового React-проекта с поддержкой TS — дело одной команды. Я рекомендую использовать шаблоны Vite + React + TS или Next.js + TS:
npm create vite [project-name] --template react-ts
npx create-next-app [project-name] --ts
Обнаружение правильного типа
Мы это уже рассматривали, но повторим еще раз: начинаем писать встроенную функцию и позволяем TS вывести правильный тип события:
При наличии сомнений относительно количества доступных параметров набираем (...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
. Именно это "не нравится" TS.
Теперь рассмотрим более сложный пример:
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>
</>
);
}
Более "чистым" способом является определение отдельных типов для принимаемых компонентом пропов:
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>
</>
);
}
Материалы для дополнительного изучения:
- Карманная книга по TypeScript;
- Шпаргалка по TypeScript;
- TypeScript в деталях;
- Шпаргалка по React + TypeScript;
- Выдержки из определений типов TypeScript для React.
Благодарю за внимание и happy coding!