Инференс типов — механизм автоматического вывода типов TypeScript на основе переданных параметров, возвращаемых значений и структуры API. В RTK Query инференс играет ключевую роль, поскольку библиотека активно использует дженерики, условные типы и вывод типов через функции.
Грамотно настроенный инференс позволяет:
RTK Query проектировался как deeply-typed API layer, поэтому значительная часть возможностей библиотеки завязана именно на выводе типов.
Основная точка входа — createApi.
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'
export const api = createApi({
reducerPath: 'api',
baseQuery: fetchBaseQuery({
baseUrl: '/api'
}),
endpoints: (builder) => ({
getUsers: builder.query({
query: () => '/users'
})
})
})
В этом примере RTK Query не знает:
Поэтому типы становятся слишком широкими:
data: unknown
arg: void
Для корректного инференса используются generic-параметры.
Сигнатура builder.query:
builder.query<ResultType, QueryArg>()
Пример:
interface User {
id: number
name: string
}
const api = createApi({
reducerPath: 'api',
baseQuery: fetchBaseQuery({
baseUrl: '/api'
}),
endpoints: (builder) => ({
getUsers: builder.query<User[], void>({
query: () => '/users'
})
})
})
Теперь RTK Query автоматически выводит:
const { data } = api.useGetUsersQuery()
Тип:
data?: User[]
Инференс распространяется на:
Второй generic отвечает за аргументы endpoint-а.
interface User {
id: number
name: string
}
const api = createApi({
reducerPath: 'api',
baseQuery: fetchBaseQuery({
baseUrl: '/api'
}),
endpoints: (builder) => ({
getUser: builder.query<User, number>({
query: (id) => `/users/${id}`
})
})
})
RTK Query выводит:
api.useGetUserQuery(5)
Корректно:
api.useGetUserQuery(5)
Ошибка:
api.useGetUserQuery('5')
TypeScript:
Argument of type 'string' is not assignable to parameter of type 'number'
Mutation работает аналогично.
interface User {
id: number
name: string
}
interface CreateUserDto {
name: string
}
const api = createApi({
reducerPath: 'api',
baseQuery: fetchBaseQuery({
baseUrl: '/api'
}),
endpoints: (builder) => ({
createUser: builder.mutation<User, CreateUserDto>({
query: (body) => ({
url: '/users',
method: 'POST',
body
})
})
})
})
Инференс:
const [createUser] = api.useCreateUserMutation()
Тип функции:
(body: CreateUserDto) => Promise<...>
RTK Query генерирует hooks автоматически.
const { data, error, isLoading } = api.useGetUsersQuery()
Типы выводятся автоматически:
data?: User[]
error?: FetchBaseQueryError | SerializedError
isLoading: boolean
Без ручной типизации.
const [createUser, result] = api.useCreateUserMutation()
RTK Query выводит:
createUser: MutationTrigger<CreateUserDto>
И:
result.data?: User
Метод unwrap() особенно важен для корректного вывода
типов.
const handleCreate = async () => {
const user = await createUser({
name: 'Alex'
}).unwrap()
console.log(user.id)
}
Тип:
user: User
Без unwrap() тип будет сложным Promise-like объектом RTK
Query.
transformResponse участвует в вычислении итогового типа
endpoint-а.
interface UserResponse {
data: User[]
}
const api = createApi({
reducerPath: 'api',
baseQuery: fetchBaseQuery({
baseUrl: '/api'
}),
endpoints: (builder) => ({
getUsers: builder.query<User[], void>({
query: () => '/users',
transformResponse: (
response: UserResponse
) => response.data
})
})
})
Тип hook-а:
data?: User[]
Несмотря на то что сервер возвращает:
{
data: User[]
}
interface ApiError {
message: string
code: string
}
const api = createApi({
reducerPath: 'api',
baseQuery: fetchBaseQuery({
baseUrl: '/api'
}),
endpoints: (builder) => ({
getUsers: builder.query<User[], void>({
query: () => '/users',
transformErrorResponse: (
response: { data: ApiError }
) => response.data
})
})
})
RTK Query корректно выводит тип transformed error.
Tags также участвуют в типизации.
type TagTypes = 'Users'
const api = createApi({
reducerPath: 'api',
tagTypes: ['Users'],
baseQuery: fetchBaseQuery({
baseUrl: '/api'
}),
endpoints: (builder) => ({
getUsers: builder.query<User[], void>({
query: () => '/users',
providesTags: ['Users']
})
})
})
Ошибка:
providesTags: ['Unknown']
TypeScript:
Type '"Unknown"' is not assignable
createUser: builder.mutation<User, CreateUserDto>({
query: (body) => ({
url: '/users',
method: 'POST',
body
}),
invalidatesTags: ['Users']
})
RTK Query автоматически связывает:
Иногда полезно извлекать типы из endpoint-ов.
type GetUsersResult =
ReturnType<typeof api.endpoints.getUsers.select>
Или:
type GetUserHook =
typeof api.useGetUserQuery
type ApiType = typeof api
Позволяет:
RTK Query генерирует strongly typed initiate actions.
dispatch(
api.endpoints.getUser.initiate(5)
)
Тип аргумента:
number
Результат:
QueryActionCreatorResult<User>
const selectUser =
api.endpoints.getUser.select(1)
Типизированный selector:
(state) => QueryResultSelectorResult<User>
В lifecycle callbacks RTK Query выводит тип cache entry автоматически.
getUser: builder.query<User, number>({
query: (id) => `/users/${id}`,
async onCacheEntryAdded(
arg,
api
) {
}
})
Типы:
arg: number
И:
api.getCacheEntry(): QueryResultSelectorResult<User>
При использовании queryFn типы становятся особенно
важными.
getUser: builder.query<User, number>({
async queryFn(id) {
try {
const response = await fetch(
`/users/${id}`
)
const data: User =
await response.json()
return { data }
} catch (error) {
return {
error: {
status: 500,
data: error
}
}
}
}
})
RTK Query выводит:
User;number;Типизация baseQuery влияет на все endpoint-ы.
const baseQuery = fetchBaseQuery({
baseUrl: '/api'
})
Тип:
BaseQueryFn<
string | FetchArgs,
unknown,
FetchBaseQueryError
>
type ApiError = {
message: string
}
const customBaseQuery:
BaseQueryFn<
string,
unknown,
ApiError
> = async (url) => {
try {
const response = await fetch(url)
const data = await response.json()
return { data }
} catch (e) {
return {
error: {
message: 'Server error'
}
}
}
}
Теперь все endpoint-ы получают:
error: ApiError
RTK Query сохраняет типы даже после расширения API.
const extendedApi = api.injectEndpoints({
endpoints: (builder) => ({
getPosts: builder.query<Post[], void>({
query: () => '/posts'
})
})
})
Типы автоматически объединяются.
extendedApi.useGetPostsQuery()
extendedApi.useGetUsersQuery()
const enhancedApi = api.enhanceEndpoints({
addTagTypes: ['Posts']
})
RTK Query расширяет union tag types.
Иногда требуется извлечь тип endpoint-а.
type GetUsersEndpoint =
typeof api.endpoints.getUsers
Полезно для:
api.useGetUsersQuery(undefined, {
pollingInterval: 5000,
skip: false
})
Options также полностью типизированы.
const [trigger, result] =
api.useLazyGetUserQuery()
Тип trigger:
(arg: number) => Promise<...>
RTK Query поддерживает специальный типизированный token.
import { skipToken } fr om '@reduxjs/toolkit/query'
const result = api.useGetUserQuery(
userId ?? skipToken
)
TypeScript корректно понимает union:
number | typeof skipToken
const { userName } =
api.useGetUserQuery(1, {
selectFromResult: ({
data
}) => ({
userName: data?.name
})
})
Тип:
userName?: string
api.useGetUsersQuery(undefined, {
pollingInterval: 1000,
refetchOnFocus: true,
refetchOnReconnect: true
})
Все параметры строго типизированы.
RTK Query экспортирует большое количество utility types.
Примеры:
QueryDefinition
MutationDefinition
BaseQueryFn
FetchArgs
FetchBaseQueryError
EndpointDefinitions
type UserQueryDefinition =
QueryDefinition<
number,
BaseQueryFn,
'Users',
User
>
Используется:
type CreateUserMutation =
MutationDefinition<
CreateUserDto,
BaseQueryFn,
'Users',
User
>
Один из наиболее сложных сценариев.
const createCrudEndpoints = <
TEntity,
TCreateDto
>(
builder: EndpointBuilder<any, any, any>,
route: string
) => ({
getAll: builder.query<TEntity[], void>({
query: () => route
}),
create: builder.mutation<
TEntity,
TCreateDto
>({
query: (body) => ({
url: route,
method: 'POST',
body
})
})
})
RTK Query способен корректно вывести generic-типы endpoint-ов.
Наиболее частые причины:
builder.query<any, any>()
После этого типизация практически исчезает.
builder.query({
query: () => '/users'
})
Результат:
unknown
transformResponse: (response) => {
return response.data
}
Без явной типизации:
response: any
string | number | boolean | object
RTK Query начинает терять точность типов.
Современный TypeScript поддерживает satisfies.
const tags = ['Users'] as const
Или:
const providesTags = (
result: User[]
) => result.map(user => ({
type: 'Users',
id: user.id
})) satisfies readonly {
type: 'Users'
id: number
}[]
Это позволяет:
tagTypes: ['Users']
Лучше:
tagTypes: ['Users'] as const
Иначе:
string[]
вместо:
readonly ['Users']
type ApiEndpoints =
keyof typeof api.endpoints
Результат:
'getUsers' | 'getUser' | 'createUser'
type AppDispatch = typeof store.dispatch
RTK Query автоматически интегрируется с dispatch typing.
type RootState =
ReturnType<typeof store.getState>
Селекторы RTK Query используют этот тип автоматически.
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware().concat(
api.middleware
)
Middleware получает типизированный store.
serializeQueryArgs: ({
endpointName,
queryArgs
}) => {
return `${endpointName}-${queryArgs}`
}
Тип queryArgs выводится из endpoint-а.
merge: (
currentCache,
newItems
) => {
currentCache.push(...newItems)
}
Типы:
currentCache: User[]
newItems: User[]
forceRefetch({
currentArg,
previousArg
}) {
return currentArg !== previousArg
}
Типы выводятся автоматически.
RTK Query использует сложную систему вложенного вывода типов:
Благодаря этому библиотека способна автоматически синхронизировать типы между:
Наиболее качественная архитектура обычно выглядит так:
export interface User {
id: number
name: string
}
export interface CreateUserDto {
name: string
}
export const api = createApi({
reducerPath: 'api',
baseQuery: fetchBaseQuery({
baseUrl: '/api'
}),
tagTypes: ['Users'] as const,
endpoints: (builder) => ({
getUsers: builder.query<
User[],
void
>({
query: () => '/users',
providesTags: ['Users']
}),
createUser: builder.mutation<
User,
CreateUserDto
>({
query: (body) => ({
url: '/users',
method: 'POST',
body
}),
invalidatesTags: ['Users']
})
})
})
В результате: