TypeScript-типы в RTK Query формируют основу строгой и предсказуемой работы с API-слоем, обеспечивая контроль над формой запросов, ответов и состояния кэша. Корректная типизация позволяет не только избежать ошибок на этапе компиляции, но и выстроить стабильную архитектуру взаимодействия с сервером, где каждый endpoint имеет явно заданный контракт.
В основе RTK Query лежит функция createApi, которая
принимает дженерики для описания типов:
createApi<
BaseQueryFn,
Endpoints,
ReducerPath,
TagTypes
>
На практике чаще всего используются первые два параметра:
BaseQueryFn — тип базового запроса (fetchBaseQuery или
кастомный)Endpoints — описание всех endpoint-овОднако ключевая типизация строится на уровне каждого endpoint.
Каждый 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.
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' });
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 позволяет преобразовывать данные и
одновременно менять тип результата.
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 позволяет формировать производный тип
данных:
builder.query<User, number>({
query: (id) => `users/${id}`,
selectFromResult: ({ data }) => ({
userName: data?.name,
}),
});
Тип результата автоматически становится:
{
userName: string | undefined;
}
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 },
],
});
Для сложных API применяется явное описание endpoints:
interface ApiEndpoints {
getUser: ReturnType<typeof builder.query<User, number>>;
createUser: ReturnType<typeof builder.mutation<User, CreateUserRequest>>;
}
Однако более современный подход — использование inference через
createApi без ручного описания интерфейсов.
RTK Query автоматически генерирует типизированные hooks:
const {
data,
error,
isLoading,
} = useGetUserQuery(1);
Типы:
data: User | undefinederror: FetchBaseQueryError | SerializedError | undefinedisLoading: booleanДля mutation:
const [createUser, result] = useCreateUserMutation();
result имеет тип:
{
data?: User;
error?: FetchBaseQueryError;
isLoading: boolean;
isSuccess: boolean;
isError: boolean;
}
При создании собственного 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 | FetchArgsunknown (можно заменить на
generic)FetchBaseQueryErrorВ крупных проектах часто используется централизованная модель типов:
type ApiResponse<T> = {
data: T;
meta?: Record<string, unknown>;
};
builder.query<ApiResponse<User>, number>({
query: (id) => `users/${id}`,
});
Такой подход обеспечивает единообразие всех endpoint-ов.
Типы напрямую влияют на:
providesTagsinvalidatesTagsselectFromResultОшибка в типах часто приводит к некорректной инвалидации данных, что делает строгую типизацию критически важной для целостности состояния.
RTK Query способен выводить тип ответа из
fetchBaseQuery, если явно указан generic:
fetchBaseQuery<User>({ baseUrl: '/api' });
В таком случае endpoint может опускать явное указание
User, но это снижает читаемость и предсказуемость
архитектуры, поэтому чаще используется явная типизация на уровне
builder.
Если endpoint не принимает аргументы:
builder.query<User[], void>({
query: () => 'users',
});
Использование:
useGetUsersQuery();
Тип void фиксирует отсутствие параметров и предотвращает
случайную передачу данных.
Стандартная структура:
builder.query<Response, Arg>({
query: (arg) => ({
url: string,
method?: string,
params?: Record<string, any>,
body?: any,
}),
});
Где:
Response — строго типизированный ответ сервераArg — структура входных данныхquery — функция с предсказуемым контрактом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,
}),
}),
}),
});
Типизация формирует строго определённый контракт между клиентом и сервером, устраняя неоднозначность данных и снижая вероятность ошибок на уровне архитектуры состояния.