Утилитарные типы библиотеки

TanStack Query активно использует сложную систему типизации TypeScript. По мере роста приложения начинают появляться повторяющиеся конструкции:

  • одинаковые типы query keys;
  • повторное описание ошибок;
  • дублирование generic-параметров;
  • необходимость извлекать типы данных из query-функций;
  • синхронизация типов между API и кешем;
  • типизация optimistic updates;
  • типизация mutation context;
  • типизация infinite queries.

Для решения этих задач библиотека предоставляет набор утилитарных типов, позволяющих:

  • извлекать типы автоматически;
  • уменьшать количество ручной типизации;
  • обеспечивать строгую типовую безопасность;
  • строить переиспользуемые abstraction layers;
  • создавать типизированные фабрики запросов.

Основные generic-параметры TanStack Query

Почти все утилитарные типы строятся вокруг базовых generic-параметров.

Типизация useQuery

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)
});

Тип QueryKey

Базовая структура

TanStack Query определяет query key как readonly-массив.

type QueryKey = readonly unknown[];

Это означает:

['users']
['users', 5]
['posts', { page: 1 }]

все являются валидными ключами.


Создание собственного типа QueryKey

Крупные приложения почти всегда создают централизованную типизацию ключей.

type AppQueryKey =
  | ['users']
  | ['users', number]
  | ['posts']
  | ['posts', { page: number }];

Использование в useQuery

useQuery<User[], Error, User[], AppQueryKey>({
  queryKey: ['users'],
  queryFn: fetchUsers
});

Тип QueryFunctionContext

Назначение

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();
};

Типизация pageParam

Особенно важна для 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

Назначение

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 Повторный запрос

Тип DefinedUseQueryResult

Отличие от UseQueryResult

UseQueryResult допускает:

data: TData | undefined

DefinedUseQueryResult гарантирует:

data: TData

Когда применяется

Используется при наличии:

  • initialData;
  • placeholderData;
  • предварительно загруженного кеша.

Пример

import { DefinedUseQueryResult } from '@tanstack/react-query';

function useUsers(): DefinedUseQueryResult<User[], Error> {
  return useQuery({
    queryKey: ['users'],
    queryFn: fetchUsers,
    initialData: []
  });
}

Тип UseInfiniteQueryResult

Назначение

Тип результата 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

Назначение

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>;

Тип UseMutationResult

Назначение

Тип результата мутации.


Структура

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
  });
}

Тип MutationFunction

Назначение

Типизация 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();
};

Тип MutationKey

Назначение

Тип ключей мутаций.


Базовая структура

type MutationKey = readonly unknown[];

Пример

const mutation = useMutation({
  mutationKey: ['create-user'],
  mutationFn: createUser
});

Тип QueryObserverResult

Назначение

Низкоуровневый тип результата query observer.


Пример

import { QueryObserverResult } from '@tanstack/react-query';

type Result = QueryObserverResult<User[], Error>;

Когда используется

Обычно применяется:

  • в кастомных abstraction layers;
  • в библиотеках поверх TanStack Query;
  • в тестовых утилитах;
  • в generic hooks.

Тип QueryObserverOptions

Назначение

Тип опций query observer.


Пример

import { QueryObserverOptions } from '@tanstack/react-query';

type Options = QueryObserverOptions<
  User[],
  Error
>;

Тип DefaultError

Назначение

Базовый тип ошибок библиотеки.


Структура

type DefaultError = Error

Использование

Если TError не указан:

const query = useQuery({
  queryKey: ['users'],
  queryFn: fetchUsers
});

ошибка автоматически получает тип:

Error

Тип PlaceholderDataFunction

Назначение

Типизация placeholderData как функции.


Пример

import { PlaceholderDataFunction } from '@tanstack/react-query';

const placeholder: PlaceholderDataFunction<User[]> =
  previousData => previousData ?? [];

Тип Updater

Назначение

Используется в setQueryData.


Структура

type Updater<TInput, TOutput>

Пример

queryClient.setQueryData<User[]>(
  ['users'],
  old => {
    if (!old) {
      return [];
    }

    return [...old, newUser];
  }
);

Функция обновления имеет тип:

Updater<User[] | undefined, User[]>

Тип QueriesOptions

Назначение

Тип массива query options.


Пример

import { QueriesOptions } from '@tanstack/react-query';

type Options = QueriesOptions<
  [
    UseQueryOptions<User[]>,
    UseQueryOptions<Post[]>
  ]
>;

Тип QueriesResults

Назначение

Типизация результатов useQueries.


Пример

import { QueriesResults } from '@tanstack/react-query';

type Results = QueriesResults<
  [
    UseQueryResult<User[], Error>,
    UseQueryResult<Post[], Error>
  ]
>;

Тип FetchQueryOptions

Назначение

Типизация queryClient.fetchQuery.


Пример

import { FetchQueryOptions } from '@tanstack/react-query';

type Options = FetchQueryOptions<
  User[],
  Error
>;

Тип EnsureQueryDataOptions

Назначение

Используется в ensureQueryData.


Пример

import { EnsureQueryDataOptions } from '@tanstack/react-query';

type Options = EnsureQueryDataOptions<
  User[],
  Error
>;

Тип SetDataOptions

Назначение

Опции setQueryData.


Пример

queryClient.setQueryData(
  ['users'],
  users,
  {
    updatedAt: Date.now()
  }
);

Тип InvalidateQueryFilters

Назначение

Типизация invalidateQueries.


Пример

import { InvalidateQueryFilters } from '@tanstack/react-query';

const filters: InvalidateQueryFilters = {
  queryKey: ['users']
};

Тип QueryFilters

Назначение

Универсальные фильтры для кеша.


Пример

const filters: QueryFilters = {
  stale: true
};

Тип MutationFilters

Назначение

Фильтрация мутаций.


Пример

const filters: MutationFilters = {
  mutationKey: ['login']
};

Извлечение типов через Awaited

TanStack Query особенно хорошо работает вместе с utility types TypeScript.


Типизация API-функции

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
});

Использование ReturnType

Извлечение результата hooks

function useUsers() {
  return useQuery({
    queryKey: ['users'],
    queryFn: fetchUsers
  });
}

Получение типа

type UsersQuery = ReturnType<typeof useUsers>;

Использование Parameters

Извлечение аргументов

type FetchUsersArgs = Parameters<typeof fetchUsers>;

Пример

const fetchUser = async (
  id: number,
  active: boolean
) => {
  return [];
};

type Args = Parameters<typeof fetchUser>;

Результат:

[number, boolean]

Использование NonNullable

Удаление undefined

В TanStack Query данные часто имеют тип:

User[] | undefined

Для безопасного извлечения:

type Users = NonNullable<
  UseQueryResult<User[]>['data']
>;

Типизация select

Базовый механизм

sel ect меняет тип данных.


Пример

const query = useQuery<
  User[],
  Error,
  string[]
>({
  queryKey: ['users'],
  queryFn: fetchUsers,
  select: users => users.map(user => user.name)
});

Ошибка без TData

useQuery<User[]>({
  select: users => users.map(user => user.name)
});

TypeScript выдаст ошибку, потому что:

string[]

не совпадает с:

User[]

Создание типизированных query factories

Проблема

Повторение query options:

useQuery({
  queryKey: ['users'],
  queryFn: fetchUsers,
  staleTime: 5000
});

Factory-подход

const usersQuery = () => ({
  queryKey: ['users'] as const,
  queryFn: fetchUsers,
  staleTime: 5000
});

Использование

useQuery(usersQuery());
queryClient.prefetchQuery(usersQuery());

Типизация query factory

Использование satisfies

const usersQuery = () =>
  ({
    queryKey: ['users'],
    queryFn: fetchUsers
  }) satisfies UseQueryOptions<User[]>;

Типизация query keys через as const

Проблема расширения массива

Без as const:

['users', 5]

получает тип:

(string | number)[]

Решение

['users', 5] as const

Результат:

readonly ['users', 5]

Типизация query key factories

Пример

export const userKeys = {
  all: ['users'] as const,

  detail: (id: number) =>
    ['users', id] as const
};

Использование

useQuery({
  queryKey: userKeys.detail(5),
  queryFn: fetchUser
});

Типизация optimistic updates

Пример

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
    );
  }
});

Типизация getQueryData

Базовый вариант

const users =
  queryClient.getQueryData<User[]>(['users']);

Проблема undefined

Метод возвращает:

User[] | undefined

потому что кеш может быть пустым.


Типизация setQueriesData

Пример

queryClient.setQueriesData<User[]>(
  {
    queryKey: ['users']
  },
  old => old ?? []
);

Типизация invalidateQueries

Пример

queryClient.invalidateQueries({
  queryKey: ['users']
});

Типизация ensureQueryData

Назначение

Гарантирует наличие данных.


Пример

const users =
  await queryClient.ensureQueryData({
    queryKey: ['users'],
    queryFn: fetchUsers
  });

TypeScript автоматически выводит:

User[]

без undefined.


Типизация queryClient

Создание клиента

const queryClient = new QueryClient();

Использование в generic helpers

function invalidateUsers(
  client: QueryClient
) {
  return client.invalidateQueries({
    queryKey: ['users']
  });
}

Типизация custom hooks

Правильный подход

function useUsers() {
  return useQuery({
    queryKey: ['users'],
    queryFn: fetchUsers
  });
}

Неправильный подход

function useUsers(): UseQueryResult<any> {
  return useQuery(...);
}

Использование any уничтожает преимущества типизации.


Типизация axios-ответов

Пример

const fetchUsers = async (): Promise<User[]> => {
  const response =
    await axios.get<User[]>('/users');

  return response.data;
};

Типизация ошибок API

Пример

type ApiError = {
  message: string;
  status: number;
};

Использование

useQuery<
  User[],
  ApiError
>({
  queryKey: ['users'],
  queryFn: fetchUsers
});

Типизация disabled queries

Проблема

Даже при:

enabled: false

тип data остаётся:

TData | undefined

Причина

Query может ещё не выполниться.


Типизация dependent queries

Пример

const userQuery = useQuery({
  queryKey: ['user', id],
  queryFn: fetchUser
});

const postsQuery = useQuery({
  queryKey: ['posts', userQuery.data?.id],
  queryFn: fetchPosts,
  enabled: !!userQuery.data
});

Типизация hydrated state

Использование dehydrate

const dehydratedState =
  dehydrate(queryClient);

Использование hydrate

hydrate(queryClient, dehydratedState);

Типизация meta

Пример

useQuery({
  queryKey: ['users'],
  queryFn: fetchUsers,
  meta: {
    requiresAuth: true
  }
});

Глубокая типизация через Register

TanStack Query v5 поддерживает глобическую регистрацию типов.


Пример

declare module '@tanstack/react-query' {
  interface Register {
    defaultError: ApiError;
  }
}

Результат

Теперь все queries автоматически используют:

ApiError

вместо стандартного Error.


Типизация skipToken

Назначение

Безопасное отключение queries.


Пример

import { skipToken } fr om '@tanstack/react-query';

const query = useQuery({
  queryKey: userId
    ? ['user', userId]
    : skipToken,
  queryFn: fetchUser
});

Использование satisfies для строгой проверки

Пример

const options = {
  queryKey: ['users'],
  queryFn: fetchUsers
} satisfies UseQueryOptions<User[]>;

Преимущества

satisfies:

  • проверяет совместимость;
  • сохраняет узкие литеральные типы;
  • не расширяет объекты;
  • безопаснее type assertion.

Проблемы чрезмерной типизации

Избыточные generics

Плохой пример:

useQuery<
  User[],
  Error,
  User[],
  ['users']
>()

при полном автоматическом выводе типов.


Лучший вариант

useQuery({
  queryKey: ['users'],
  queryFn: fetchUsers
});

Когда generics действительно нужны

Явная типизация полезна:

  • при использовании select;
  • при кастомных ошибках;
  • при сложных query keys;
  • при reusable hooks;
  • при abstraction layers;
  • при работе с infinite queries;
  • при optimistic updates;
  • при создании SDK поверх TanStack Query.