TanStack Query активно использует сложную систему типизации TypeScript. По мере роста приложения начинают появляться повторяющиеся конструкции:
Для решения этих задач библиотека предоставляет набор утилитарных типов, позволяющих:
Почти все утилитарные типы строятся вокруг базовых generic-параметров.
useQuery<
TQueryFnData,
TError,
TData,
TQueryKey
>()
| Generic | Назначение |
|---|---|
| TQueryFnData | Исходные данные queryFn |
| TError | Тип ошибки |
| TData | Трансформированные данные |
| TQueryKey | Тип query key |
type User = {
id: number;
name: string;
};
type ApiError = {
message: string;
};
const query = useQuery<
User[],
ApiError,
string[],
['users']
>({
queryKey: ['users'],
queryFn: fetchUsers,
select: users => users.map(user => user.name)
});
TanStack Query определяет query key как readonly-массив.
type QueryKey = readonly unknown[];
Это означает:
['users']
['users', 5]
['posts', { page: 1 }]
все являются валидными ключами.
Крупные приложения почти всегда создают централизованную типизацию ключей.
type AppQueryKey =
| ['users']
| ['users', number]
| ['posts']
| ['posts', { page: number }];
useQuery<User[], Error, User[], AppQueryKey>({
queryKey: ['users'],
queryFn: fetchUsers
});
QueryFunctionContext описывает объект, который TanStack Query передаёт в queryFn.
type QueryFunctionContext<
TQueryKey = QueryKey,
TPageParam = never
>
import { QueryFunctionContext } from '@tanstack/react-query';
type UserKey = ['user', number];
const fetchUser = async (
context: QueryFunctionContext<UserKey>
) => {
const [, id] = context.queryKey;
const response = await fetch(`/api/users/${id}`);
return response.json();
};
Особенно важна для infinite queries.
type PostsKey = ['posts'];
const fetchPosts = async (
context: QueryFunctionContext<PostsKey, number>
) => {
const page = context.pageParam;
const response = await fetch(`/api/posts?page=${page}`);
return response.json();
};
UseQueryResult описывает объект, который возвращает useQuery.
type UseQueryResult<TData, TError>
import { UseQueryResult } from '@tanstack/react-query';
type User = {
id: number;
name: string;
};
function useUsers(): UseQueryResult<User[], Error> {
return useQuery({
queryKey: ['users'],
queryFn: fetchUsers
});
}
| Свойство | Назначение |
|---|---|
| data | Данные |
| error | Ошибка |
| isLoading | Загрузка |
| isFetching | Фоновый запрос |
| isSuccess | Успешное состояние |
| isError | Ошибка |
| refetch | Повторный запрос |
UseQueryResult допускает:
data: TData | undefined
DefinedUseQueryResult гарантирует:
data: TData
Используется при наличии:
import { DefinedUseQueryResult } from '@tanstack/react-query';
function useUsers(): DefinedUseQueryResult<User[], Error> {
return useQuery({
queryKey: ['users'],
queryFn: fetchUsers,
initialData: []
});
}
Тип результата infinite queries.
import { UseInfiniteQueryResult } from '@tanstack/react-query';
type Post = {
id: number;
title: string;
};
function usePosts(): UseInfiniteQueryResult<Post[], Error> {
return useInfiniteQuery({
queryKey: ['posts'],
queryFn: fetchPosts,
initialPageParam: 1,
getNextPageParam: lastPage => lastPage.nextCursor
});
}
InfiniteData описывает структуру данных infinite query.
type InfiniteData<TData, TPageParam>
{
pages: TData[];
pageParams: TPageParam[];
}
import { InfiniteData } from '@tanstack/react-query';
type PostPage = {
items: Post[];
nextCursor?: number;
};
type PostsData = InfiniteData<PostPage, number>;
Тип результата мутации.
type UseMutationResult<
TData,
TError,
TVariables,
TContext
>
type CreateUserDto = {
name: string;
};
type User = {
id: number;
name: string;
};
function useCreateUser(): UseMutationResult<
User,
Error,
CreateUserDto,
unknown
> {
return useMutation({
mutationFn: createUser
});
}
Типизация mutationFn.
type MutationFunction<TData, TVariables>
import { MutationFunction } from '@tanstack/react-query';
type LoginDto = {
email: string;
password: string;
};
type AuthResponse = {
token: string;
};
const login: MutationFunction<
AuthResponse,
LoginDto
> = async credentials => {
const response = await fetch('/api/login', {
method: 'POST',
body: JSON.stringify(credentials)
});
return response.json();
};
Тип ключей мутаций.
type MutationKey = readonly unknown[];
const mutation = useMutation({
mutationKey: ['create-user'],
mutationFn: createUser
});
Низкоуровневый тип результата query observer.
import { QueryObserverResult } from '@tanstack/react-query';
type Result = QueryObserverResult<User[], Error>;
Обычно применяется:
Тип опций query observer.
import { QueryObserverOptions } from '@tanstack/react-query';
type Options = QueryObserverOptions<
User[],
Error
>;
Базовый тип ошибок библиотеки.
type DefaultError = Error
Если TError не указан:
const query = useQuery({
queryKey: ['users'],
queryFn: fetchUsers
});
ошибка автоматически получает тип:
Error
Типизация placeholderData как функции.
import { PlaceholderDataFunction } from '@tanstack/react-query';
const placeholder: PlaceholderDataFunction<User[]> =
previousData => previousData ?? [];
Используется в setQueryData.
type Updater<TInput, TOutput>
queryClient.setQueryData<User[]>(
['users'],
old => {
if (!old) {
return [];
}
return [...old, newUser];
}
);
Функция обновления имеет тип:
Updater<User[] | undefined, User[]>
Тип массива query options.
import { QueriesOptions } from '@tanstack/react-query';
type Options = QueriesOptions<
[
UseQueryOptions<User[]>,
UseQueryOptions<Post[]>
]
>;
Типизация результатов useQueries.
import { QueriesResults } from '@tanstack/react-query';
type Results = QueriesResults<
[
UseQueryResult<User[], Error>,
UseQueryResult<Post[], Error>
]
>;
Типизация queryClient.fetchQuery.
import { FetchQueryOptions } from '@tanstack/react-query';
type Options = FetchQueryOptions<
User[],
Error
>;
Используется в ensureQueryData.
import { EnsureQueryDataOptions } from '@tanstack/react-query';
type Options = EnsureQueryDataOptions<
User[],
Error
>;
Опции setQueryData.
queryClient.setQueryData(
['users'],
users,
{
updatedAt: Date.now()
}
);
Типизация invalidateQueries.
import { InvalidateQueryFilters } from '@tanstack/react-query';
const filters: InvalidateQueryFilters = {
queryKey: ['users']
};
Универсальные фильтры для кеша.
const filters: QueryFilters = {
stale: true
};
Фильтрация мутаций.
const filters: MutationFilters = {
mutationKey: ['login']
};
TanStack Query особенно хорошо работает вместе с utility types TypeScript.
const fetchUsers = async () => {
const response = await fetch('/api/users');
return response.json() as Promise<User[]>;
};
type Users = Awaited<
ReturnType<typeof fetchUsers>
>;
const query = useQuery<Users>({
queryKey: ['users'],
queryFn: fetchUsers
});
function useUsers() {
return useQuery({
queryKey: ['users'],
queryFn: fetchUsers
});
}
type UsersQuery = ReturnType<typeof useUsers>;
type FetchUsersArgs = Parameters<typeof fetchUsers>;
const fetchUser = async (
id: number,
active: boolean
) => {
return [];
};
type Args = Parameters<typeof fetchUser>;
Результат:
[number, boolean]
В TanStack Query данные часто имеют тип:
User[] | undefined
Для безопасного извлечения:
type Users = NonNullable<
UseQueryResult<User[]>['data']
>;
sel ect меняет тип данных.
const query = useQuery<
User[],
Error,
string[]
>({
queryKey: ['users'],
queryFn: fetchUsers,
select: users => users.map(user => user.name)
});
useQuery<User[]>({
select: users => users.map(user => user.name)
});
TypeScript выдаст ошибку, потому что:
string[]
не совпадает с:
User[]
Повторение query options:
useQuery({
queryKey: ['users'],
queryFn: fetchUsers,
staleTime: 5000
});
const usersQuery = () => ({
queryKey: ['users'] as const,
queryFn: fetchUsers,
staleTime: 5000
});
useQuery(usersQuery());
queryClient.prefetchQuery(usersQuery());
const usersQuery = () =>
({
queryKey: ['users'],
queryFn: fetchUsers
}) satisfies UseQueryOptions<User[]>;
Без as const:
['users', 5]
получает тип:
(string | number)[]
['users', 5] as const
Результат:
readonly ['users', 5]
export const userKeys = {
all: ['users'] as const,
detail: (id: number) =>
['users', id] as const
};
useQuery({
queryKey: userKeys.detail(5),
queryFn: fetchUser
});
type Context = {
previousUsers?: User[];
};
useMutation<
User,
Error,
CreateUserDto,
Context
>({
mutationFn: createUser,
onMutate: async newUser => {
const previousUsers =
queryClient.getQueryData<User[]>(['users']);
return { previousUsers };
},
onError: (
error,
variables,
context
) => {
queryClient.setQueryData(
['users'],
context?.previousUsers
);
}
});
const users =
queryClient.getQueryData<User[]>(['users']);
Метод возвращает:
User[] | undefined
потому что кеш может быть пустым.
queryClient.setQueriesData<User[]>(
{
queryKey: ['users']
},
old => old ?? []
);
queryClient.invalidateQueries({
queryKey: ['users']
});
Гарантирует наличие данных.
const users =
await queryClient.ensureQueryData({
queryKey: ['users'],
queryFn: fetchUsers
});
TypeScript автоматически выводит:
User[]
без undefined.
const queryClient = new QueryClient();
function invalidateUsers(
client: QueryClient
) {
return client.invalidateQueries({
queryKey: ['users']
});
}
function useUsers() {
return useQuery({
queryKey: ['users'],
queryFn: fetchUsers
});
}
function useUsers(): UseQueryResult<any> {
return useQuery(...);
}
Использование any уничтожает преимущества типизации.
const fetchUsers = async (): Promise<User[]> => {
const response =
await axios.get<User[]>('/users');
return response.data;
};
type ApiError = {
message: string;
status: number;
};
useQuery<
User[],
ApiError
>({
queryKey: ['users'],
queryFn: fetchUsers
});
Даже при:
enabled: false
тип data остаётся:
TData | undefined
Query может ещё не выполниться.
const userQuery = useQuery({
queryKey: ['user', id],
queryFn: fetchUser
});
const postsQuery = useQuery({
queryKey: ['posts', userQuery.data?.id],
queryFn: fetchPosts,
enabled: !!userQuery.data
});
const dehydratedState =
dehydrate(queryClient);
hydrate(queryClient, dehydratedState);
useQuery({
queryKey: ['users'],
queryFn: fetchUsers,
meta: {
requiresAuth: true
}
});
TanStack Query v5 поддерживает глобическую регистрацию типов.
declare module '@tanstack/react-query' {
interface Register {
defaultError: ApiError;
}
}
Теперь все queries автоматически используют:
ApiError
вместо стандартного Error.
Безопасное отключение queries.
import { skipToken } fr om '@tanstack/react-query';
const query = useQuery({
queryKey: userId
? ['user', userId]
: skipToken,
queryFn: fetchUser
});
const options = {
queryKey: ['users'],
queryFn: fetchUsers
} satisfies UseQueryOptions<User[]>;
satisfies:
Плохой пример:
useQuery<
User[],
Error,
User[],
['users']
>()
при полном автоматическом выводе типов.
useQuery({
queryKey: ['users'],
queryFn: fetchUsers
});
Явная типизация полезна: