Usage With TypeScript
What You'll Learn
- Details on how to use various RTK Query APIs with TypeScript
Introduction
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+.
- TypeScript
- JavaScript
// Need to use the React-specific entry point to allow generating React hooks
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'
import type { Pokemon } from './types'
// Define a service using a base URL and expected endpoints
export const pokemonApi = createApi({
reducerPath: 'pokemonApi',
baseQuery: fetchBaseQuery({ baseUrl: 'https://pokeapi.co/api/v2/' }),
endpoints: (builder) => ({
getPokemonByName: builder.query<Pokemon, string>({
query: (name) => `pokemon/${name}`,
}),
}),
})
// Export hooks for usage in function components, which are
// auto-generated based on the defined endpoints
export const { useGetPokemonByNameQuery } = pokemonApi
// Need to use the React-specific entry point to allow generating React hooks
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'
// Define a service using a base URL and expected endpoints
export const pokemonApi = createApi({
reducerPath: 'pokemonApi',
baseQuery: fetchBaseQuery({ baseUrl: 'https://pokeapi.co/api/v2/' }),
endpoints: (builder) => ({
getPokemonByName: builder.query({
query: (name) => `pokemon/${name}`,
}),
}),
})
// Export hooks for usage in function components, which are
// auto-generated based on the defined endpoints
export const { useGetPokemonByNameQuery } = pokemonApi
For older versions of TS, you can use api.endpoints.[endpointName].useQuery/useMutation
to access the same hooks.
- TypeScript
- JavaScript
import { pokemonApi } from './pokemon'
const useGetPokemonByNameQuery = pokemonApi.endpoints.getPokemonByName.useQuery
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.
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 aquery
property on an endpoint will be passed here.Result
- The type to be returned in thedata
property for the success case. Unless you expect all queries and mutations to return the same type, it is recommended to keep this typed asunknown
, and specify the types individually as shown below.Error
- The type to be returned for theerror
property in the error case. This type also applies to allqueryFn
functions used in endpoints throughout the API definition.DefinitionExtraOptions
- The type for the third parameter of the function. The value provided to theextraOptions
property on an endpoint will be passed here.Meta
- the type of themeta
property that may be returned from calling thebaseQuery
. Themeta
property is accessible as the second argument totransformResponse
.
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
- TypeScript
- JavaScript
import { createApi } from '@reduxjs/toolkit/query'
import type { BaseQueryFn } from '@reduxjs/toolkit/query'
const simpleBaseQuery: BaseQueryFn<
string, // Args
unknown, // Result
{ reason: string }, // Error
{ shout?: boolean }, // DefinitionExtraOptions
{ timestamp: number } // Meta
> = (arg, api, extraOptions) => {
// `arg` has the type `string`
// `api` has the type `BaseQueryApi` (not configurable)
// `extraOptions` has the type `{ shout?: boolean }
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: (builder) => ({
getSupport: builder.query({
query: () => 'support me',
extraOptions: {
shout: true,
},
}),
}),
})
import { createApi } from '@reduxjs/toolkit/query'
const simpleBaseQuery = (arg, api, extraOptions) => {
// `arg` has the type `string`
// `api` has the type `BaseQueryApi` (not configurable)
// `extraOptions` has the type `{ shout?: boolean }
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: (builder) => ({
getSupport: builder.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 optionaltransformResponse
.- If
transformResponse
is not provided, then it is treated as though a successful query will return this type instead. - If
transformResponse
is provided, the input type fortransformResponse
must also be specified, to indicate the type that the initial query returns. The return type fortransformResponse
must matchResultType
. - If
queryFn
is used rather thanquery
, then it must return the following shape for the success case:{
data: ResultType
}
- If
QueryArg
- The type of the input that will be passed as the only parameter to thequery
property of the endpoint, or the first parameter of aqueryFn
property if used instead.- If
query
doesn't have a parameter, thenvoid
type has to be provided explicitly. - If
query
has an optional parameter, then a union type with the type of parameter, andvoid
has to be provided, e.g.number | void
.
- If
- TypeScript
- JavaScript
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'
interface Post {
id: number
name: string
}
const api = createApi({
baseQuery: fetchBaseQuery({ baseUrl: '/' }),
endpoints: (build) => ({
// ResultType QueryArg
// v v
getPost: build.query<Post, number>({
// inferred as `number` from the `QueryArg` type
// v
query: (id) => `post/${id}`,
// An explicit type must be provided to the raw result that the query returns
// when using `transformResponse`
// v
transformResponse: (rawResult: { result: { post: Post } }, meta) => {
// ^
// The optional `meta` property is available based on the type for the `baseQuery` used
// The return value for `transformResponse` must match `ResultType`
return rawResult.result.post
},
}),
}),
})
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'
const api = createApi({
baseQuery: fetchBaseQuery({ baseUrl: '/' }),
endpoints: (build) => ({
// ResultType QueryArg
// v v
getPost: build.query({
// inferred as `number` from the `QueryArg` type
// v
query: (id) => `post/${id}`,
// An explicit type must be provided to the raw result that the query returns
// when using `transformResponse`
// v
transformResponse: (rawResult, meta) => {
// ^
// The optional `meta` property is available based on the type for the `baseQuery` used
// The return value for `transformResponse` must match `ResultType`
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
.
Typing a 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.
- TypeScript
- JavaScript
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) => ({
// ResultType QueryArg
// v v
getPost: build.query<Post, number>({
// inferred as `number` from the `QueryArg` type
// v
queryFn: (arg, queryApi, extraOptions, baseQuery) => {
const post: Post = {
id: arg,
name: getRandomName(),
}
// For the success case, the return type for the `data` property
// must match `ResultType`
// v
return { data: post }
},
}),
}),
})
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'
import { getRandomName } from './randomData'
const api = createApi({
baseQuery: fetchBaseQuery({ baseUrl: '/' }),
endpoints: (build) => ({
// ResultType QueryArg
// v v
getPost: build.query({
// inferred as `number` from the `QueryArg` type
// v
queryFn: (arg, queryApi, extraOptions, baseQuery) => {
const post = {
id: arg,
name: getRandomName(),
}
// For the success case, the return type for the `data` property
// must match `ResultType`
// v
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:
{
status: number
data: any
}
An error case for the example above using queryFn
and the error type from fetchBaseQuery
could look like:
- TypeScript
- JavaScript
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 }
},
}),
}),
})
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.
- TypeScript
- JavaScript
import { createApi, fakeBaseQuery } from '@reduxjs/toolkit/query'
type CustomErrorType = { reason: 'too cold' | 'too hot' }
const api = createApi({
// This type will be used as the error type for all `queryFn` functions provided
// v
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!' }
},
}),
}),
})
import { createApi, fakeBaseQuery } from '@reduxjs/toolkit/query'
const api = createApi({
// This type will be used as the error type for all `queryFn` functions provided
// v
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 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:
- result:
ResultType
|undefined
- The result returned by a successful query. The type corresponds withResultType
as supplied to the built endpoint. In the error case for a query, this will beundefined
. - error:
ErrorType
|undefined
- The error returned by an errored query. The type corresponds withError
as supplied to thebaseQuery
for the api. In the success case for a query, this will beundefined
. - arg:
QueryArg
- The argument supplied to thequery
property when the query itself is called. The type corresponds withQueryArg
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
.
- TypeScript
- JavaScript
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' }],
}),
}),
})
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.
- TypeScript
- JavaScript
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'
import type { Post } from './types'
export const api = createApi({
baseQuery: fetchBaseQuery({ baseUrl: '/' }),
endpoints: (build) => ({
// Query argument is required to be `number`, and can't be `undefined`
// V
getPost: build.query<Post, number>({
query: (id) => `post/${id}`,
}),
}),
})
export const { useGetPostQuery } = api
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'
export const api = createApi({
baseQuery: fetchBaseQuery({ baseUrl: '/' }),
endpoints: (build) => ({
// Query argument is required to be `number`, and can't be `undefined`
// V
getPost: build.query({
query: (id) => `post/${id}`,
}),
}),
})
export const { useGetPostQuery } = api
import { useGetPostQuery } from './api'
function MaybePost({ id }: { id?: number }) {
// This will produce a typescript error:
// Argument of type 'number | undefined' is not assignable to parameter of type 'number | unique symbol'.
// Type 'undefined' is not assignable to type 'number | unique symbol'.
// @ts-expect-error id passed must be a number, but we don't call it when it isn't a 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.
import { skipToken } from '@reduxjs/toolkit/query/react'
import { useGetPostQuery } from './api'
function MaybePost({ id }: { id?: number }) {
// When `id` is nullish, we will still skip the query.
// TypeScript is also happy that the query will only ever be called with a `number` now
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.
- TypeScript
- JavaScript
export type FetchBaseQueryError =
| {
/**
* * `number`:
* HTTP status code
*/
status: number
data: unknown
}
| {
/**
* * `"FETCH_ERROR"`:
* An error that occurred during execution of `fetch` or the `fetchFn` callback option
**/
status: 'FETCH_ERROR'
data?: undefined
error: string
}
| {
/**
* * `"PARSING_ERROR"`:
* An error happened during parsing.
* Most likely a non-JSON-response was returned with the default `responseHandler` "JSON",
* or an error occurred while executing a custom `responseHandler`.
**/
status: 'PARSING_ERROR'
originalStatus: number
data: string
error: string
}
| {
/**
* * `"CUSTOM_ERROR"`:
* A custom error type that you can return from your `queryFn` where another error might not make sense.
**/
status: 'CUSTOM_ERROR'
data?: unknown
error: string
}
export {}
- TypeScript
- JavaScript
export interface SerializedError {
name?: string
message?: string
stack?: string
code?: string
}
export {}
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 { api } from './services/api'
function PostDetail() {
const { data, error, isLoading } = usePostsQuery()
if (isLoading) {
return <div>Loading...</div>
}
if (error) {
if ('status' in error) {
// you can access all properties of `FetchBaseQueryError` here
const errMsg = 'error' in error ? error.error : JSON.stringify(error.data)
return (
<div>
<div>An error has occurred:</div>
<div>{errMsg}</div>
</div>
)
}
else {
// you can access all properties of `SerializedError` here
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.
import { FetchBaseQueryError } from '@reduxjs/toolkit/query'
/**
* Type predicate to narrow an unknown error to `FetchBaseQueryError`
*/
export function isFetchBaseQueryError(
error: unknown
): error is FetchBaseQueryError {
return typeof error === 'object' && error != null && 'status' in error
}
/**
* Type predicate to narrow an unknown error to an object with a string 'message' property
*/
export function isErrorWithMessage(
error: unknown
): error is { message: string } {
return (
typeof error === 'object' &&
error != null &&
'message' in error &&
typeof (error as any).message === 'string'
)
}
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)) {
// you can access all properties of `FetchBaseQueryError` here
const errMsg = 'error' in err ? err.error : JSON.stringify(err.data)
enqueueSnackbar(errMsg, { variant: 'error' })
} else if (isErrorWithMessage(err)) {
// you can access a string 'message' property here
enqueueSnackbar(err.message, { variant: 'error' })
}
}
}
return (
<div>
<input value={name} onChange={(e) => setName(e.target.value)} />
<button>Add post</button>
</div>
)
}