Типизация данных запроса
Сегодня мы научимся типизировать запросы, ответы и научимся использовать Generics в React Query для работы с TypeScript.
Начнём с запроса к API. Допустим, у нас есть API, который возвращает список пользователей:
[
{
"id": 1,
"name": "John Doe",
"email": "john.doe@example.com"
},
{
"id": 2,
"name": "Jane Smith",
"email": "jane.smith@example.com"
}
]
Первое, что мы сделаем, — задекларируем интерфейс для этих данных. Это позволит нам гарантировать, что мы знаем структуру ответа.
interface User {
id: number;
name: string;
email: string;
}
Теперь, когда у нас есть структура, мы можем использовать её в React Query. Представим, что мы получаем данные с помощью useQuery:
import { useQuery } from '@tanstack/react-query';
import axios from 'axios';
const fetchUsers = async (): Promise<User[]> => {
const response = await axios.get('/api/users');
return response.data;
};
const UsersList = () => {
const { data, isLoading, error, isError } = useQuery<User[]м({
queryKey: ['users'],
queryFn: fetchUsers,
});
if (isLoading) {
return <p>Loading...</p>;
}
if (isError && error instanceof Error) {
return <p>Error: {error.message}</p>;
}
return (
<ul>
{data?.map((user) => (
<li key={user.id}>
{user.name} ({user.email})
</li>
))}
</ul>
);
};
export default UsersList;
Разбираем код:
- Мы указали
User[]как обобщённый тип (Generic) дляuseQuery. Это гарантирует, чтоdataбудет массивом объектов типаUser. - Функция
fetchUsersвозвращаетPromise<User[]>, что явно показывает, какие данные мы ожидаем от API. - Теперь TypeScript предупреждает нас, если мы неправильно работаем с данными, например, пытаемся получить свойство, которого нет в интерфейсе
User.
Типизация данных, которые мы отправляем
Иногда нам нужно не только получать данные, но и отправлять их на сервер, например, при создании нового пользователя. Допустим, API принимает следующий JSON:
{
"name": "Alice Johnson",
"email": "alice.johnson@example.com"
}
Для этого мы создадим интерфейс:
interface CreateUserInput {
name: string;
email: string;
}
А также интерфейс для ответа сервера, если он возвращает созданного пользователя:
interface CreateUserResponse {
id: number;
name: string;
email: string;
}
Теперь мы можем использовать эти интерфейсы при создании мутации с помощью useMutation:
import { useMutation } from '@tanstack/react-query';
import axios from 'axios';
const createUser = async (input: CreateUserInput): Promise<CreateUserResponse> => {
const response = await axios.post('/api/users', input);
return response.data;
};
const CreateUserForm = () => {
const mutation = useMutation<CreateUserResponse, Error, CreateUserInput>({
mutationFn: createUser,
});
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
const formData = new FormData(e.currentTarget);
const input: CreateUserInput = {
name: formData.get('name') as string,
email: formData.get('email') as string,
};
mutation.mutate(input);
};
if (mutation.isPending) {
return <p>Saving...</p>;
}
if (mutation.isError) {
return <p>Error: {mutation.error.message}</p>;
}
if (mutation.isSuccess) {
return <p>User created: {mutation.data?.name}</p>;
}
return (
<form onSubmit={handleSubmit}>
<input name="name" placeholder="Name" required />
<input name="email" placeholder="Email" type="email" required />
<button type="submit">Create</button>
</form>
);
};
export default CreateUserForm;
Что мы сделали:
- Типизировали входные данные запросов
CreateUserInputи ожидаемый результат ответаCreateUserResponse. - В компоненте
CreateUserFormуказали эти типы вuseMutationпри помощи Generics:- Первый тип
CreateUserResponse— это результат мутации. - Второй тип
Error— тип ошибки при выполнении запроса. - Третий тип
CreateUserInput— тип данных, которые отправляются на сервер.
- Первый тип
Типизация данных с неизвестной структурой
Иногда мы можем столкнуться с API, где структура данных не определена заранее. Например, если мы получаем универсальный ответ с различными данными в зависимости от запроса. Для таких случаев можно использовать TypeScript-синтаксис Record<string, any> или схему проверки типов.
Пример:
type ApiResponse = Record<string, any>;
// Пример использования:
const fetchDynamicData = async (): Promise<ApiResponse> => {
const response = await axios.get('/api/dynamic');
return response.data;
};
Здесь ApiResponse указывает, что мы получаем объект с произвольными ключами и значениями.
Особенности работы с типами в React Query
Иногда разработчики совершают ошибки при типизации данных. Например:
- Не указывают тип вообще, полагаясь на
any. Это может привести к отсутствию проверки типов и проблемам в будущем. - Забывают типизировать возможные состояния загрузки или ошибок. Например, не всегда обрабатывают
errorилиisLoading, из-за чего пользовательский интерфейс может оказаться незавершённым.
Чтобы избежать этих проблем, старайтесь всегда использовать Generics для типизации useQuery, useMutation и других методов React Query.
Применение в реальных проектах
Типизация данных особенно полезна в сложных проектах, где взаимодействие с сервером происходит часто. Она помогает избежать ошибок на продакшене, улучшает читаемость кода и делает работу команды более продуктивной. Кроме того, она повышает вашу ценность как разработчика на собеседованиях — умение писать безопасный код с TypeScript сегодня востребовано почти в любом проекте.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ