Механизм transformResponse используется для
преобразования данных, полученных от сервера, перед сохранением
результата в кэш RTK Query. В простых случаях достаточно указать тип
возвращаемого значения, однако в сложных приложениях появляются
дополнительные уровни типизации:
Базовый пример:
interface UserApi {
id: number
first_name: string
last_name: string
}
interface User {
id: number
fullName: string
}
const api = createApi({
reducerPath: 'api',
baseQuery: fetchBaseQuery({
baseUrl: '/api'
}),
endpoints: (builder) => ({
getUsers: builder.query<User[], void>({
query: () => '/users',
transformResponse: (response: UserApi[]): User[] => {
return response.map(user => ({
id: user.id,
fullName: `${user.first_name} ${user.last_name}`
}))
}
})
})
})
Тип результата transformResponse становится итоговым
типом endpoint.
Сигнатура transformResponse:
transformResponse(
baseQueryReturnValue,
meta,
arg
)
Полная типизация:
transformResponse: (
response: ApiResponse,
meta: FetchBaseQueryMeta | undefined,
arg: QueryArg
) => ResultType
Пример:
interface ApiPost {
id: number
title: string
created_at: string
}
interface Post {
id: number
title: string
createdAt: Date
}
type GetPostsArg = {
limit: number
}
getPosts: builder.query<Post[], GetPostsArg>({
query: ({ limit }) => `/posts?limit=${limit}`,
transformResponse: (
response: ApiPost[],
meta,
arg
): Post[] => {
console.log(meta)
console.log(arg.limit)
return response.map(post => ({
id: post.id,
title: post.title,
createdAt: new Date(post.created_at)
}))
}
})
Большинство REST API возвращают не массив напрямую, а обёртку:
{
"data": [],
"meta": {},
"success": true
}
Типизация:
interface ApiListResponse<T> {
data: T[]
meta: {
total: number
page: number
}
success: boolean
}
interface ApiUser {
id: number
email: string
}
interface User {
id: number
email: string
}
getUsers: builder.query<User[], void>({
query: () => '/users',
transformResponse: (
response: ApiListResponse<ApiUser>
): User[] => {
return response.data.map(user => ({
id: user.id,
email: user.email
}))
}
})
Создание повторно используемых трансформеров:
interface ApiEntity {
id: number
}
interface ApiListResponse<T> {
data: T[]
}
function extractList<T>(
response: ApiListResponse<T>
): T[] {
return response.data
}
Использование:
getUsers: builder.query<User[], void>({
query: () => '/users',
transformResponse: extractList<User>
})
Более сложный вариант:
function transformCollection<ApiType, ResultType>(
response: ApiListResponse<ApiType>,
mapper: (item: ApiType) => ResultType
): ResultType[] {
return response.data.map(mapper)
}
Использование:
transformResponse: (response: ApiListResponse<ApiUser>) => {
return transformCollection(response, user => ({
id: user.id,
email: user.email
}))
}
RTK Query часто используется вместе с
createEntityAdapter.
Типизированная нормализация:
import {
createEntityAdapter,
EntityState
} from '@reduxjs/toolkit'
interface User {
id: number
name: string
}
const usersAdapter = createEntityAdapter<User>()
const initialState = usersAdapter.getInitialState()
type UsersState = EntityState<User, number>
getUsers: builder.query<UsersState, void>({
query: () => '/users',
transformResponse: (
response: User[]
): UsersState => {
return usersAdapter.setAll(
initialState,
response
)
}
})
Теперь endpoint возвращает:
{
ids: number[]
entities: Record<number, User>
}
Трансформация может добавлять вычисляемые значения.
Пример:
interface ApiProduct {
id: number
price: number
discount: number
}
interface Product {
id: number
price: number
discount: number
finalPrice: number
}
transformResponse: (
response: ApiProduct[]
): Product[] => {
return response.map(product => ({
...product,
finalPrice:
product.price -
product.price * product.discount
}))
}
Сервер обычно возвращает даты строками.
Типизация:
interface ApiOrder {
id: number
created_at: string
}
interface Order {
id: number
createdAt: Date
}
transformResponse: (
response: ApiOrder
): Order => {
return {
id: response.id,
createdAt: new Date(response.created_at)
}
}
Массовое преобразование:
type WithDates<T> = {
[K in keyof T]:
T[K] extends string
? Date | string
: T[K]
}
Условные типы позволяют создавать универсальные преобразования.
Пример:
type ApiResult<T> =
T extends string
? { value: string }
: T extends number
? { value: number }
: never
Использование:
function transformValue<T>(
value: ApiResult<T>
): T {
return value.value as T
}
Трансформации могут зависеть от типа объекта.
Пример API:
type ApiNotification =
| {
type: 'email'
email: string
}
| {
type: 'sms'
phone: string
}
Финальная модель:
type Notification =
| {
type: 'email'
target: string
}
| {
type: 'sms'
target: string
}
Трансформация:
transformResponse: (
response: ApiNotification[]
): Notification[] => {
return response.map(item => {
switch (item.type) {
case 'email':
return {
type: 'email',
target: item.email
}
case 'sms':
return {
type: 'sms',
target: item.phone
}
}
})
}
TypeScript корректно сузит тип внутри switch.
Многие API возвращают null.
Пример:
interface ApiProfile {
id: number
avatar: string | null
}
interface Profile {
id: number
avatarUrl?: string
}
Трансформация:
transformResponse: (
response: ApiProfile
): Profile => {
return {
id: response.id,
avatarUrl:
response.avatar ?? undefined
}
}
PartialИногда API возвращает неполные структуры.
Пример:
interface User {
id: number
name: string
email: string
}
Частичный ответ:
type PartialUser = Partial<User>
Использование:
transformResponse: (
response: PartialUser[]
): PartialUser[] => {
return response.filter(user => user.id)
}
Создание сложных универсальных преобразований:
interface ApiResponse<T> {
data: T
}
type ExtractData<T> =
T extends ApiResponse<infer U>
? U
: never
Пример:
type UserData =
ExtractData<ApiResponse<User>>
Результат:
type UserData = User
transformErrorResponseRTK Query позволяет типизировать преобразование ошибок.
Пример:
interface ApiError {
status: string
message: string
}
interface AppError {
code: string
text: string
}
Трансформация:
transformErrorResponse: (
response: ApiError
): AppError => {
return {
code: response.status,
text: response.message
}
}
Использование:
const { error } = useGetUsersQuery()
if (error) {
console.log(error.code)
}
fetchBaseQuery может возвращать meta.
Пример:
transformResponse: (
response: User[],
meta
): User[] => {
console.log(meta?.request)
console.log(meta?.response)
return response
}
Тип meta:
FetchBaseQueryMeta
Пример сложного API:
{
"users": [],
"roles": []
}
Типизация:
interface ApiUser {
id: number
role_id: number
}
interface ApiRole {
id: number
name: string
}
interface ApiResponse {
users: ApiUser[]
roles: ApiRole[]
}
Финальная модель:
interface User {
id: number
role: string
}
Трансформация:
transformResponse: (
response: ApiResponse
): User[] => {
return response.users.map(user => {
const role = response.roles.find(
role => role.id === user.role_id
)
return {
id: user.id,
role: role?.name ?? 'unknown'
}
})
}
Picktype UserPreview =
Pick<User, 'id' | 'name'>
Omittype SafeUser =
Omit<User, 'password'>
Recordtype UsersMap =
Record<number, User>
Readonlytype ImmutableUser =
Readonly<User>
API может возвращать смешанные коллекции.
Пример:
type ApiFeedItem =
| ApiPost
| ApiComment
Трансформация:
type FeedItem =
| Post
| Comment
transformResponse: (
response: ApiFeedItem[]
): FeedItem[] => {
return response.map(item => {
if ('title' in item) {
return {
id: item.id,
title: item.title
}
}
return {
id: item.id,
text: item.text
}
})
}
transformResponse должен быть синхронным. Возврат
Promise не поддерживается.
Некорректно:
transformResponse: async (response) => {
return await process(response)
}
Корректный подход:
queryFn: async () => {
const response = await fetch('/users')
const data = await response.json()
return {
data: transformUsers(data)
}
}
ReturnTypeПример:
function mapUser(user: ApiUser) {
return {
id: user.id,
fullName:
`${user.first_name} ${user.last_name}`
}
}
type User =
ReturnType<typeof mapUser>
Использование:
transformResponse: (
response: ApiUser[]
): User[] => {
return response.map(mapUser)
}
typeofПример:
const defaultUser = {
id: 0,
name: ''
}
type User = typeof defaultUser
Создание универсального helper:
function createTransformer<Api, Result>(
mapper: (value: Api) => Result
) {
return (response: Api[]): Result[] => {
return response.map(mapper)
}
}
Использование:
const transformUsers =
createTransformer<ApiUser, User>(
user => ({
id: user.id,
fullName:
`${user.first_name} ${user.last_name}`
})
)
Применение:
transformResponse: transformUsers
Сложные ответы:
interface ApiResponse {
data: {
users: {
items: ApiUser[]
}
}
}
Трансформация:
transformResponse: (
response: ApiResponse
): User[] => {
return response
.data
.users
.items
.map(user => ({
id: user.id,
name: user.name
}))
}
Пример:
type ImmutableUsers =
ReadonlyArray<
Readonly<User>
>
Трансформация:
transformResponse: (
response: User[]
): ImmutableUsers => {
return response
}
Создание type guard:
function isAdmin(
user: User | Admin
): user is Admin {
return 'permissions' in user
}
Использование:
transformResponse: (
response: Array<User | Admin>
): Admin[] => {
return response.filter(isAdmin)
}
TypeScript автоматически выведет Admin[].
Разделение преобразований:
const normalizeUser = (
user: ApiUser
): User => {
return {
id: user.id,
name: user.name.trim()
}
}
Дополнительная стадия:
const sortUsers = (
users: User[]
): User[] => {
return users.sort(
(a, b) =>
a.name.localeCompare(b.name)
)
}
Использование:
transformResponse: (
response: ApiUser[]
): User[] => {
return sortUsers(
response.map(normalizeUser)
)
}
Пример:
interface ApiPage<T> {
items: T[]
nextCursor: string | null
}
Финальный тип:
interface PageResult<T> {
data: T[]
hasMore: boolean
}
Трансформация:
transformResponse: (
response: ApiPage<User>
): PageResult<User> => {
return {
data: response.items,
hasMore: response.nextCursor !== null
}
}
Пример:
type Nullable<T> = {
[K in keyof T]:
T[K] | null
}
Использование:
type NullableUser =
Nullable<User>
Некорректно:
builder.query<User[], void>({
transformResponse: (
response: ApiUser[]
): User => {
return {
id: 1,
name: 'Test'
}
}
})
Endpoint ожидает User[], а возвращается
User.
Некорректно:
function extract(response: any) {
return response.data
}
Правильно:
function extract<T>(
response: ApiResponse<T>
): T {
return response.data
}
anyПлохой пример:
transformResponse: (
response: any
): any => {
return response
}
Последствия:
Крупные проекты обычно разделяют:
Типичная структура:
shared/
api/
mappers/
transformers/
types/
adapters/
Пример mapper-файла:
export function mapUser(
user: ApiUser
): User {
return {
id: user.id,
name: user.name
}
}
Использование в endpoint:
transformResponse: (
response: ApiUser[]
): User[] => {
return response.map(mapUser)
}