Типизация запросов и ответов

TypeScript-типы в RTK Query формируют основу строгой и предсказуемой работы с API-слоем, обеспечивая контроль над формой запросов, ответов и состояния кэша. Корректная типизация позволяет не только избежать ошибок на этапе компиляции, но и выстроить стабильную архитектуру взаимодействия с сервером, где каждый endpoint имеет явно заданный контракт.


В основе RTK Query лежит функция createApi, которая принимает дженерики для описания типов:

createApi<
  BaseQueryFn,
  Endpoints,
  ReducerPath,
  TagTypes
>

На практике чаще всего используются первые два параметра:

  • BaseQueryFn — тип базового запроса (fetchBaseQuery или кастомный)
  • Endpoints — описание всех endpoint-ов

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


Типизация данных ответа (Response)

Каждый query или mutation endpoint должен явно описывать возвращаемый тип данных через queryFn или builder.query<TResponse, TArg>.

Пример простого запроса

type User = {
  id: number;
  name: string;
  email: string;
};

const api = createApi({
  reducerPath: 'api',
  baseQuery: fetchBaseQuery({ baseUrl: '/api' }),
  endpoints: (builder) => ({
    getUser: builder.query<User, number>({
      query: (id) => `users/${id}`,
    }),
  }),
});

Здесь:

  • User — тип ответа
  • number — тип аргумента запроса

RTK Query автоматически выводит типы хука:

const { data } = useGetUserQuery(1);

data имеет тип User | undefined.


Типизация аргументов запроса

Аргумент запроса задаётся вторым параметром дженерика:

builder.query<ResponseType, ArgType>

Пример с объектным аргументом

type GetUsersArgs = {
  page: number;
  limit: number;
};

type User = {
  id: number;
  name: string;
};

const api = createApi({
  baseQuery: fetchBaseQuery({ baseUrl: '/api' }),
  endpoints: (builder) => ({
    getUsers: builder.query<User[], GetUsersArgs>({
      query: ({ page, limit }) => ({
        url: 'users',
        params: { page, limit },
      }),
    }),
  }),
});

Аргумент строго типизирован:

useGetUsersQuery({ page: 1, limit: 20 });

Любое несоответствие структуры приводит к ошибке TypeScript.


Типизация mutation-запросов

Mutations имеют аналогичную сигнатуру:

builder.mutation<ResponseType, ArgType>

Пример создания сущности

type CreateUserRequest = {
  name: string;
  email: string;
};

type User = {
  id: number;
  name: string;
  email: string;
};

const api = createApi({
  baseQuery: fetchBaseQuery({ baseUrl: '/api' }),
  endpoints: (builder) => ({
    createUser: builder.mutation<User, CreateUserRequest>({
      query: (body) => ({
        url: 'users',
        method: 'POST',
        body,
      }),
    }),
  }),
});

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

const [createUser, result] = useCreateUserMutation();

createUser({ name: 'Alex', email: 'a@mail.com' });

Типизация baseQuery и обработка ошибок

fetchBaseQuery возвращает результат типа:

{
  data?: T;
  error?: FetchBaseQueryError;
}

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

type FetchBaseQueryError =
  | {
      status: number;
      dat a: unknown;
    }
  | {
      status: 'FETCH_ERROR';
      error: string;
    }
  | {
      status: 'PARSING_ERROR';
      originalStatus: number;
      dat a: string;
      error: string;
    };

Пример безопасной обработки

const { data, error } = useGetUserQuery(1);

if (error) {
  if ('status' in error) {
    console.log(error.status);
  }
}

Типизация transformResponse

transformResponse позволяет преобразовывать данные и одновременно менять тип результата.

builder.query<OutputType, InputArg>({
  query: () => 'users',
  transformResponse: (response: { data: User[] }) => {
    return response.data.map(u => u.name);
  },
});

Типы:

  • входной ответ: { data: User[] }
  • выходной тип: string[]
getUserNames: builder.query<string[], void>({
  query: () => 'users',
  transformResponse: (response: { data: User[] }) =>
    response.data.map((u) => u.name),
});

Типизация selectFromResult

selectFromResult позволяет формировать производный тип данных:

builder.query<User, number>({
  query: (id) => `users/${id}`,
  selectFromResult: ({ data }) => ({
    userName: data?.name,
  }),
});

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

{
  userName: string | undefined;
}

Типизация tagTypes и инвалидации

RTK Query позволяет строго типизировать теги кэша:

type TagTypes = 'User' | 'Post';
createApi<
  BaseQueryFn,
  Endpoints,
  ReducerPath,
  TagTypes
>

Пример использования:

builder.query<User, number>({
  query: (id) => `users/${id}`,
  providesTags: (result, error, id) => [
    { type: 'User', id },
  ],
});

Mutations:

builder.mutation<User, Partial<User>>({
  query: (body) => ({
    url: 'users',
    method: 'PATCH',
    body,
  }),
  invalidatesTags: (result, error, body) => [
    { type: 'User', id: body.id },
  ],
});

Типизация endpoints через интерфейсы

Для сложных API применяется явное описание endpoints:

interface ApiEndpoints {
  getUser: ReturnType<typeof builder.query<User, number>>;
  createUser: ReturnType<typeof builder.mutation<User, CreateUserRequest>>;
}

Однако более современный подход — использование inference через createApi без ручного описания интерфейсов.


Типизация hooks

RTK Query автоматически генерирует типизированные hooks:

const {
  data,
  error,
  isLoading,
} = useGetUserQuery(1);

Типы:

  • data: User | undefined
  • error: FetchBaseQueryError | SerializedError | undefined
  • isLoading: boolean

Для mutation:

const [createUser, result] = useCreateUserMutation();

result имеет тип:

{
  data?: User;
  error?: FetchBaseQueryError;
  isLoading: boolean;
  isSuccess: boolean;
  isError: boolean;
}

Типизация кастомного baseQuery

При создании собственного baseQuery важно описывать:

const customBaseQuery: BaseQueryFn<
  string | FetchArgs,
  unknown,
  FetchBaseQueryError
> = async (args, api, extraOptions) => {
  const result = await fetch(args as string);
  return { data: await result.json() };
};

Здесь:

  • аргумент запроса: string | FetchArgs
  • успешный результат: unknown (можно заменить на generic)
  • ошибка: FetchBaseQueryError

Генерация строгих API-контрактов

В крупных проектах часто используется централизованная модель типов:

type ApiResponse<T> = {
  data: T;
  meta?: Record<string, unknown>;
};
builder.query<ApiResponse<User>, number>({
  query: (id) => `users/${id}`,
});

Такой подход обеспечивает единообразие всех endpoint-ов.


Связь типизации с кэшированием

Типы напрямую влияют на:

  • providesTags
  • invalidatesTags
  • selectFromResult
  • нормализацию кэша

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


Инференс типов из baseQuery

RTK Query способен выводить тип ответа из fetchBaseQuery, если явно указан generic:

fetchBaseQuery<User>({ baseUrl: '/api' });

В таком случае endpoint может опускать явное указание User, но это снижает читаемость и предсказуемость архитектуры, поэтому чаще используется явная типизация на уровне builder.


Типизация void аргументов

Если endpoint не принимает аргументы:

builder.query<User[], void>({
  query: () => 'users',
});

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

useGetUsersQuery();

Тип void фиксирует отсутствие параметров и предотвращает случайную передачу данных.


Обобщённая модель типизации endpoint

Стандартная структура:

builder.query<Response, Arg>({
  query: (arg) => ({
    url: string,
    method?: string,
    params?: Record<string, any>,
    body?: any,
  }),
});

Где:

  • Response — строго типизированный ответ сервера
  • Arg — структура входных данных
  • query — функция с предсказуемым контрактом

Итоговая структура типового API-слайса

type User = {
  id: number;
  name: string;
};

type CreateUser = {
  name: string;
};

const api = createApi({
  baseQuery: fetchBaseQuery({ baseUrl: '/api' }),
  endpoints: (builder) => ({
    getUser: builder.query<User, number>({
      query: (id) => `users/${id}`,
    }),
    createUser: builder.mutation<User, CreateUser>({
      query: (body) => ({
        url: 'users',
        method: 'POST',
        body,
      }),
    }),
  }),
});

Типизация формирует строго определённый контракт между клиентом и сервером, устраняя неоднозначность данных и снижая вероятность ошибок на уровне архитектуры состояния.