What You'll Learn
As with the rest of the Redux Toolkit package, RTK Query is written in TypeScript, and its API is designed for seamless use in TypeScript applications.
This page provides details for using APIs included in RTK Query with TypeScript and how to type them correctly.
info
We strongly recommend using TypeScript 4.1+ with RTK Query for best results.
If you encounter any problems with the types that are not described on this page, please open an issue for discussion.
createApi
Using auto-generated React Hooks
The React-specific entry point for RTK Query exports a version of createApi
which automatically generates React hooks for each of the defined query & mutation endpoints
.
To use the auto-generated React Hooks as a TypeScript user, you'll need to use TS4.1+.
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'
import type { Pokemon } from './types'
export const pokemonApi = createApi({
reducerPath: 'pokemonApi',
baseQuery: fetchBaseQuery({ baseUrl: 'https://pokeapi.co/api/v2/' }),
endpoints: (build) => ({
getPokemonByName: build.query<Pokemon, string>({
query: (name) => `pokemon/${name}`,
}),
}),
})
export const { useGetPokemonByNameQuery } = pokemonApi
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'
export const pokemonApi = createApi({
reducerPath: 'pokemonApi',
baseQuery: fetchBaseQuery({ baseUrl: 'https://pokeapi.co/api/v2/' }),
endpoints: (build) => ({
getPokemonByName: build.query({
query: (name) => `pokemon/${name}`,
}),
}),
})
export const { useGetPokemonByNameQuery } = pokemonApi
For older versions of TS, you can use api.endpoints.[endpointName].useQuery/useMutation
to access the same hooks.
Accessing api hooks directly
import { pokemonApi } from './pokemon'
const useGetPokemonByNameQuery = pokemonApi.endpoints.getPokemonByName.useQuery
Accessing api hooks directly
import { pokemonApi } from './pokemon'
const useGetPokemonByNameQuery = pokemonApi.endpoints.getPokemonByName.useQuery
Typing a baseQuery
Typing a custom baseQuery
can be done using the BaseQueryFn
type exported by RTK Query.
Base Query signature
export type BaseQueryFn<
Args = any,
Result = unknown,
Error = unknown,
DefinitionExtraOptions = {},
Meta = {},
> = (
args: Args,
api: BaseQueryApi,
extraOptions: DefinitionExtraOptions,
) => MaybePromise<QueryReturnValue<Result, Error, Meta>>
export interface BaseQueryApi {
signal: AbortSignal
dispatch: ThunkDispatch<any, any, any>
getState: () => unknown
}
export type QueryReturnValue<T = unknown, E = unknown, M = unknown> =
| {
error: E
data?: undefined
meta?: M
}
| {
error?: undefined
data: T
meta?: M
}
The BaseQueryFn
type accepts the following generics:
Args
- The type for the first parameter of the function. The result returned by a query
property on an endpoint will be passed here.Result
- The type to be returned in the data
property for the success case. Unless you expect all queries and mutations to return the same type, it is recommended to keep this typed as unknown
, and specify the types individually as shown below.Error
- The type to be returned for the error
property in the error case. This type also applies to all queryFn
functions used in endpoints throughout the API definition.DefinitionExtraOptions
- The type for the third parameter of the function. The value provided to the extraOptions
property on an endpoint will be passed here.Meta
- the type of the meta
property that may be returned from calling the baseQuery
. The meta
property is accessible as the second argument to transformResponse
and transformErrorResponse
.note
The meta
property returned from a baseQuery
will always be considered as potentially undefined, as a throw
in the error case may result in it not being provided. When accessing values from the meta
property, this should be accounted for, e.g. using optional chaining
Simple baseQuery TypeScript example
import { createApi } from '@reduxjs/toolkit/query'
import type { BaseQueryFn } from '@reduxjs/toolkit/query'
const simpleBaseQuery: BaseQueryFn<
string,
unknown,
{ reason: string },
{ shout?: boolean },
{ timestamp: number }
> = (arg, api, extraOptions) => {
const meta = { timestamp: Date.now() }
if (arg === 'forceFail') {
return {
error: {
reason: 'Intentionally requested to fail!',
meta,
},
}
}
if (extraOptions.shout) {
return { data: 'CONGRATULATIONS', meta }
}
return { data: 'congratulations', meta }
}
const api = createApi({
baseQuery: simpleBaseQuery,
endpoints: (build) => ({
getSupport: build.query({
query: () => 'support me',
extraOptions: {
shout: true,
},
}),
}),
})
Simple baseQuery TypeScript example
import { createApi } from '@reduxjs/toolkit/query'
const simpleBaseQuery = (arg, api, extraOptions) => {
const meta = { timestamp: Date.now() }
if (arg === 'forceFail') {
return {
error: {
reason: 'Intentionally requested to fail!',
meta,
},
}
}
if (extraOptions.shout) {
return { data: 'CONGRATULATIONS', meta }
}
return { data: 'congratulations', meta }
}
const api = createApi({
baseQuery: simpleBaseQuery,
endpoints: (build) => ({
getSupport: build.query({
query: () => 'support me',
extraOptions: {
shout: true,
},
}),
}),
})
Typing query and mutation endpoints
endpoints
for an api are defined as an object using the builder syntax. Both query
and mutation
endpoints can be typed by providing types to the generics in <ResultType, QueryArg>
format.
ResultType
- The type of the final data returned by the query, factoring an optional transformResponse
.
transformResponse
is not provided, then it is treated as though a successful query will return this type instead.transformResponse
is provided, the input type for transformResponse
must also be specified, to indicate the type that the initial query returns. The return type for transformResponse
must match ResultType
.queryFn
is used rather than query
, then it must return the following shape for the success case:QueryArg
- The type of the input that will be passed as the only parameter to the query
property of the endpoint, or the first parameter of a queryFn
property if used instead.
query
doesn't have a parameter, then void
type has to be provided explicitly.query
has an optional parameter, then a union type with the type of parameter, and void
has to be provided, e.g. number | void
.Defining endpoints with TypeScript
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'
interface Post {
id: number
name: string
}
const api = createApi({
baseQuery: fetchBaseQuery({ baseUrl: '/' }),
endpoints: (build) => ({
getPost: build.query<Post, number>({
query: (id) => `post/${id}`,
transformResponse: (rawResult: { result: { post: Post } }, meta) => {
return rawResult.result.post
},
}),
}),
})
Defining endpoints with TypeScript
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'
const api = createApi({
baseQuery: fetchBaseQuery({ baseUrl: '/' }),
endpoints: (build) => ({
getPost: build.query({
query: (id) => `post/${id}`,
transformResponse: (rawResult, meta) => {
return rawResult.result.post
},
}),
}),
})
note
queries
and mutations
can also have their return type defined by a baseQuery
rather than the method shown above, however, unless you expect all of your queries and mutations to return the same type, it is recommended to leave the return type of the baseQuery
as unknown
.
queryFn
As mentioned in Typing query and mutation endpoints, a queryFn
will receive its result & arg types from the generics provided to the corresponding built endpoint.
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'
import { getRandomName } from './randomData'
interface Post {
id: number
name: string
}
const api = createApi({
baseQuery: fetchBaseQuery({ baseUrl: '/' }),
endpoints: (build) => ({
getPost: build.query<Post, number>({
queryFn: (arg, queryApi, extraOptions, baseQuery) => {
const post: Post = {
id: arg,
name: getRandomName(),
}
return { data: post }
},
}),
}),
})
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'
import { getRandomName } from './randomData'
const api = createApi({
baseQuery: fetchBaseQuery({ baseUrl: '/' }),
endpoints: (build) => ({
getPost: build.query({
queryFn: (arg, queryApi, extraOptions, baseQuery) => {
const post = {
id: arg,
name: getRandomName(),
}
return { data: post }
},
}),
}),
})
The error type that a queryFn
must return is determined by the baseQuery
provided to createApi
.
With fetchBaseQuery
, the error type is like so:
fetchBaseQuery error shape
{
status: number
data: any
}
An error case for the example above using queryFn
and the error type from fetchBaseQuery
could look like:
queryFn error example with error type from fetchBaseQuery
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'
import { getRandomName } from './randomData'
interface Post {
id: number
name: string
}
const api = createApi({
baseQuery: fetchBaseQuery({ baseUrl: '/' }),
endpoints: (build) => ({
getPost: build.query<Post, number>({
queryFn: (arg, queryApi, extraOptions, baseQuery) => {
if (arg <= 0) {
return {
error: {
status: 500,
statusText: 'Internal Server Error',
data: 'Invalid ID provided.',
},
}
}
const post: Post = {
id: arg,
name: getRandomName(),
}
return { data: post }
},
}),
}),
})
queryFn error example with error type from fetchBaseQuery
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'
import { getRandomName } from './randomData'
const api = createApi({
baseQuery: fetchBaseQuery({ baseUrl: '/' }),
endpoints: (build) => ({
getPost: build.query({
queryFn: (arg, queryApi, extraOptions, baseQuery) => {
if (arg <= 0) {
return {
error: {
status: 500,
statusText: 'Internal Server Error',
data: 'Invalid ID provided.',
},
}
}
const post = {
id: arg,
name: getRandomName(),
}
return { data: post }
},
}),
}),
})
For users who wish to only use queryFn
for each endpoint and not include a baseQuery
at all, RTK Query provides a fakeBaseQuery
function that can be used to easily specify the error type each queryFn
should return.
Excluding baseQuery for all endpoints
import { createApi, fakeBaseQuery } from '@reduxjs/toolkit/query'
type CustomErrorType = { reason: 'too cold' | 'too hot' }
const api = createApi({
baseQuery: fakeBaseQuery<CustomErrorType>(),
endpoints: (build) => ({
eatPorridge: build.query<'just right', 1 | 2 | 3>({
queryFn(seat) {
if (seat === 1) {
return { error: { reason: 'too cold' } }
}
if (seat === 2) {
return { error: { reason: 'too hot' } }
}
return { data: 'just right' }
},
}),
microwaveHotPocket: build.query<'delicious!', number>({
queryFn(duration) {
if (duration < 110) {
return { error: { reason: 'too cold' } }
}
if (duration > 140) {
return { error: { reason: 'too hot' } }
}
return { data: 'delicious!' }
},
}),
}),
})
Excluding baseQuery for all endpoints
import { createApi, fakeBaseQuery } from '@reduxjs/toolkit/query'
const api = createApi({
baseQuery: fakeBaseQuery(),
endpoints: (build) => ({
eatPorridge: build.query({
queryFn(seat) {
if (seat === 1) {
return { error: { reason: 'too cold' } }
}
if (seat === 2) {
return { error: { reason: 'too hot' } }
}
return { data: 'just right' }
},
}),
microwaveHotPocket: build.query({
queryFn(duration) {
if (duration < 110) {
return { error: { reason: 'too cold' } }
}
if (duration > 140) {
return { error: { reason: 'too hot' } }
}
return { data: 'delicious!' }
},
}),
}),
})
Typing dispatch
and getState
createApi
exposes the standard Redux dispatch
and getState
methods in several places, such as the lifecycleApi
argument in lifecycle methods, or the baseQueryApi
argument passed to queryFn
methods and base query functions.
Normally, your application infers RootState
and AppDispatch
types from the store setup. Since createApi
has to be called prior to creating the Redux store and is used as part of the store setup sequence, it can't directly know or use those types - it would cause a circular type inference error.
By default, dispatch
usages inside of createApi
will be typed as ThunkDispatch
, and getState
usages are typed as () => unknown
. You will need to assert the type when needed - getState() as RootState
. You may also include an explicit return type for the function as well, in order to break the circular type inference cycle:
const api = createApi({
baseQuery,
endpoints: (build) => ({
getTodos: build.query<Todo[], void>({
async queryFn() {
const state = getState() as RootState
const text = state.todoTexts[queryFnCalls]
return { data: [{ id: `${queryFnCalls++}`, text }] }
},
}),
}),
})
Typing providesTags
/invalidatesTags
RTK Query utilizes a cache tag invalidation system in order to provide automated re-fetching of stale data.
When using the function notation, both the providesTags
and invalidatesTags
properties on endpoints are called with the following arguments:
ResultType
| undefined
- The result returned by a successful query. The type corresponds with ResultType
as supplied to the built endpoint. In the error case for a query, this will be undefined
.ErrorType
| undefined
- The error returned by an errored query. The type corresponds with Error
as supplied to the baseQuery
for the api. In the success case for a query, this will be undefined
.QueryArg
- The argument supplied to the query
property when the query itself is called. The type corresponds with QueryArg
as supplied to the built endpoint.A recommended use-case with providesTags
when a query returns a list of items is to provide a tag for each item in the list using the entity ID, as well as a 'LIST' ID tag (see Advanced Invalidation with abstract tag IDs).
This is often written by spreading the result of mapping the received data into an array, as well as an additional item in the array for the 'LIST'
ID tag. When spreading the mapped array, by default, TypeScript will broaden the type
property to string
. As the tag type
must correspond to one of the string literals provided to the tagTypes
property of the api, the broad string
type will not satisfy TypeScript. In order to alleviate this, the tag type
can be cast as const
to prevent the type being broadened to string
.
providesTags TypeScript example
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'
interface Post {
id: number
name: string
}
type PostsResponse = Post[]
const api = createApi({
baseQuery: fetchBaseQuery({ baseUrl: '/' }),
tagTypes: ['Posts'],
endpoints: (build) => ({
getPosts: build.query<PostsResponse, void>({
query: () => 'posts',
providesTags: (result) =>
result
? [
...result.map(({ id }) => ({ type: 'Posts' as const, id })),
{ type: 'Posts', id: 'LIST' },
]
: [{ type: 'Posts', id: 'LIST' }],
}),
}),
})
providesTags TypeScript example
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'
const api = createApi({
baseQuery: fetchBaseQuery({ baseUrl: '/' }),
tagTypes: ['Posts'],
endpoints: (build) => ({
getPosts: build.query({
query: () => 'posts',
providesTags: (result) =>
result
? [
...result.map(({ id }) => ({ type: 'Posts', id })),
{ type: 'Posts', id: 'LIST' },
]
: [{ type: 'Posts', id: 'LIST' }],
}),
}),
})
Skipping queries with TypeScript using skipToken
RTK Query provides the ability to conditionally skip queries from automatically running using the skip
parameter as part of query hook options (see Conditional Fetching).
TypeScript users may find that they encounter invalid type scenarios when a query argument is typed to not be undefined
, and they attempt to skip
the query when an argument would not be valid.
API definition
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'
import type { Post } from './types'
export const api = createApi({
baseQuery: fetchBaseQuery({ baseUrl: '/' }),
endpoints: (build) => ({
getPost: build.query<Post, number>({
query: (id) => `post/${id}`,
}),
}),
})
export const { useGetPostQuery } = api
API definition
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'
export const api = createApi({
baseQuery: fetchBaseQuery({ baseUrl: '/' }),
endpoints: (build) => ({
getPost: build.query({
query: (id) => `post/${id}`,
}),
}),
})
export const { useGetPostQuery } = api
Using skip in a component
import { useGetPostQuery } from './api'
function MaybePost({ id }: { id?: number }) {
const { data } = useGetPostQuery(id, { skip: !id })
return <div>...</div>
}
While you might be able to convince yourself that the query won't be called unless the id
arg is a number
at the time, TypeScript won't be convinced so easily.
RTK Query provides a skipToken
export which can be used as an alternative to the skip
option in order to skip queries, while remaining type-safe. When skipToken
is passed as the query argument to useQuery
, useQueryState
or useQuerySubscription
, it provides the same effect as setting skip: true
in the query options, while also being a valid argument in scenarios where the arg
might be undefined otherwise.
Using skipToken in a component
import { skipToken } from '@reduxjs/toolkit/query/react'
import { useGetPostQuery } from './api'
function MaybePost({ id }: { id?: number }) {
const { data } = useGetPostQuery(id ?? skipToken)
return <div>...</div>
}
Type safe error handling
When an error is gracefully provided from a base query
, RTK query will provide the error directly. If an unexpected error is thrown by user code rather than a handled error, that error will be transformed into a SerializedError
shape. Users should make sure that they are checking which kind of error they are dealing with before attempting to access its properties. This can be done in a type safe manner either by using a type guard, e.g. by checking for discriminated properties, or using a type predicate.
When using fetchBaseQuery
, as your base query, errors will be of type FetchBaseQueryError | SerializedError
. The specific shapes of those types can be seen below.
FetchBaseQueryError type
export type FetchBaseQueryError =
| {
status: number
data: unknown
}
| {
status: 'FETCH_ERROR'
data?: undefined
error: string
}
| {
status: 'PARSING_ERROR'
originalStatus: number
data: string
error: string
}
| {
status: 'CUSTOM_ERROR'
data?: unknown
error: string
}
SerializedError type
export interface SerializedError {
name?: string
message?: string
stack?: string
code?: string
}
Error result example
When using fetchBaseQuery
, the error
property returned from a hook will have the type FetchBaseQueryError | SerializedError | undefined
. If an error is present, you can access error properties after narrowing the type to either FetchBaseQueryError
or SerializedError
.
import { usePostsQuery } from './services/api'
function PostDetail() {
const { data, error, isLoading } = usePostsQuery()
if (isLoading) {
return <div>Loading...</div>
}
if (error) {
if ('status' in error) {
const errMsg = 'error' in error ? error.error : JSON.stringify(error.data)
return (
<div>
<div>An error has occurred:</div>
<div>{errMsg}</div>
</div>
)
}
return <div>{error.message}</div>
}
if (data) {
return (
<div>
{data.map((post) => (
<div key={post.id}>Name: {post.name}</div>
))}
</div>
)
}
return null
}
Inline error handling example
When handling errors inline after unwrapping
a mutation call, a thrown error will have a type of any
for typescript versions below 4.4, or unknown
for versions 4.4+. In order to safely access properties of the error, you must first narrow the type to a known type. This can be done using a type predicate as shown below.
services/helpers.ts
import { FetchBaseQueryError } from '@reduxjs/toolkit/query'
export function isFetchBaseQueryError(
error: unknown,
): error is FetchBaseQueryError {
return typeof error === 'object' && error != null && 'status' in error
}
export function isErrorWithMessage(
error: unknown,
): error is { message: string } {
return (
typeof error === 'object' &&
error != null &&
'message' in error &&
typeof (error as any).message === 'string'
)
}
addPost.tsx
import { useState } from 'react'
import { useSnackbar } from 'notistack'
import { api } from './services/api'
import { isFetchBaseQueryError, isErrorWithMessage } from './services/helpers'
function AddPost() {
const { enqueueSnackbar, closeSnackbar } = useSnackbar()
const [name, setName] = useState('')
const [addPost] = useAddPostMutation()
async function handleAddPost() {
try {
await addPost(name).unwrap()
setName('')
} catch (err) {
if (isFetchBaseQueryError(err)) {
const errMsg = 'error' in err ? err.error : JSON.stringify(err.data)
enqueueSnackbar(errMsg, { variant: 'error' })
} else if (isErrorWithMessage(err)) {
enqueueSnackbar(err.message, { variant: 'error' })
}
}
}
return (
<div>
<input value={name} onChange={(e) => setName(e.target.value)} />
<button>Add post</button>
</div>
)
}
Schema Validation
Endpoints can have schemas for runtime validation of query args, responses, and errors. Any Standard Schema compliant library can be used. See API reference for full list of available schemas.
When following the default approach of explicitly specifying type parameters for queries and mutations, the schemas will be required to match the types provided.
Explicitly typed endpoint
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'
import * as v from 'valibot'
const postSchema = v.object({
id: v.number(),
name: v.string(),
})
type Post = v.InferOutput<typeof postSchema>
const api = createApi({
baseQuery: fetchBaseQuery({ baseUrl: '/' }),
endpoints: (build) => ({
getPost: build.query<Post, { id: number }>({
query: ({ id }) => `/post/${id}`,
responseSchema: postSchema,
}),
}),
})
Schemas can also be used as a source of inference, meaning that the type parameters can be omitted.
Implicitly typed endpoint
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'
import * as v from 'valibot'
const postSchema = v.object({
id: v.number(),
name: v.string(),
})
type Post = v.InferOutput<typeof postSchema>
const api = createApi({
baseQuery: fetchBaseQuery({ baseUrl: '/' }),
endpoints: (build) => ({
getPost: build.query({
query: ({ id }: { id: number }) => `/post/${id}`,
responseSchema: postSchema,
}),
getTransformedPost: build.query({
query: ({ id }: { id: number }) => `/post/${id}`,
rawResponseSchema: postSchema,
transformResponse: (response) => ({
...response,
published_at: new Date(response.published_at),
}),
}),
}),
})
warning
Schemas should not perform any transformation that would change the type of the value.
Incorrect usage
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'
import * as v from 'valibot'
import { titleCase } from 'lodash'
const postSchema = v.object({
id: v.number(),
name: v.pipe(
v.string(),
v.transform(titleCase),
),
published_at: v.pipe(
v.string(),
v.transform((s) => new Date(s)),
v.date(),
),
})
type Post = v.InferOutput<typeof postSchema>
const api = createApi({
baseQuery: fetchBaseQuery({ baseUrl: '/' }),
endpoints: (build) => ({
getPost: build.query<Post, { id: number }>({
query: ({ id }) => `/post/${id}`,
responseSchema: postSchema,
}),
}),
})
Instead, transformation should be done with transformResponse
and transformErrorResponse
(when using query
) or inside queryFn
(when using queryFn
).
Correct usage
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'
import * as v from 'valibot'
const postSchema = v.object({
id: v.number(),
name: v.string(),
published_at: v.string(),
})
type RawPost = v.InferOutput<typeof postSchema>
type Post = Omit<RawPost, 'published_at'> & { published_at: Date }
const api = createApi({
baseQuery: fetchBaseQuery({ baseUrl: '/' }),
endpoints: (build) => ({
getPost: build.query<Post, { id: number }>({
query: ({ id }) => `/post/${id}`,
rawResponseSchema: postSchema,
transformResponse: (response) => ({
...response,
published_at: new Date(response.published_at),
}),
}),
}),
})
RetroSearch is an open source project built by @garambo | Open a GitHub Issue
Search and Browse the WWW like it's 1997 | Search results from DuckDuckGo
HTML:
3.2
| Encoding:
UTF-8
| Version:
0.7.4