使用 TypeScript
- 有关如何使用各种 RTK Query API 与 TypeScript 的详细信息
介绍
与 Redux Toolkit 包的其他部分一样,RTK Query 是用 TypeScript 编写的,其 API 旨在无缝地用于 TypeScript 应用程序。
本页提供了有关如何使用 RTK Query 中包含的 API 与 TypeScript 以及如何正确对其进行类型化的详细信息。
我们强烈建议使用 TypeScript 4.1+ 与 RTK Query 以获得最佳效果。
如果您遇到此页面上未描述的类型问题,请打开一个问题进行讨论。
createApi
使用自动生成的 React Hooks
RTK Query 的 React 特定入口点导出 createApi
的版本,它会为每个定义的查询和变异 endpoints
自动生成 React Hooks。
要使用自动生成的 React Hooks 作为 TypeScript 用户,您需要使用 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
对于旧版本的 TS,您可以使用 api.endpoints.[endpointName].useQuery/useMutation
来访问相同的 Hooks。
- TypeScript
- JavaScript
import { pokemonApi } from './pokemon'
const useGetPokemonByNameQuery = pokemonApi.endpoints.getPokemonByName.useQuery
import { pokemonApi } from './pokemon'
const useGetPokemonByNameQuery = pokemonApi.endpoints.getPokemonByName.useQuery
为 baseQuery
键入
为自定义 baseQuery
键入可以使用 RTK Query 导出的 BaseQueryFn
类型。
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
}
BaseQueryFn
类型接受以下泛型
Args
- 函数第一个参数的类型。端点上query
属性返回的结果将在此处传递。Result
- 在成功情况下要返回的data
属性的类型。除非您希望所有查询和变异都返回相同的类型,否则建议将此键入为unknown
,并如 下面 所示分别指定类型。Error
- 在错误情况下要返回的error
属性的类型。此类型也适用于整个 API 定义中端点使用的所有queryFn
函数。DefinitionExtraOptions
- 函数第三个参数的类型。传递给端点上的extraOptions
属性的值将在此处传递。Meta
- 从调用baseQuery
返回的meta
属性的类型。meta
属性可以通过transformResponse
和transformErrorResponse
作为第二个参数访问。
从 baseQuery
返回的 meta
属性将始终被视为可能未定义,因为错误情况下的 throw
可能会导致它未被提供。访问 meta
属性中的值时,应考虑到这一点,例如使用 可选链
- 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,
},
}),
}),
})
为查询和变异 endpoints
键入
API 的 endpoints
使用构建器语法定义为一个对象。query
和 mutation
endpoints
都可以通过为 <ResultType, QueryArg>
格式的泛型提供类型来键入。
ResultType
- 查询返回的最终数据的类型,考虑可选的transformResponse
。- 如果未提供
transformResponse
,则它将被视为成功的查询将返回此类型。 - 如果提供了
transformResponse
,则还必须指定transformResponse
的输入类型,以指示初始查询返回的类型。transformResponse
的返回类型必须与ResultType
匹配。 - 如果使用
queryFn
而不是query
,则它必须为成功情况返回以下形状{
data: ResultType
}
- 如果未提供
QueryArg
- 将作为唯一参数传递给endpoint
的query
属性的输入类型,或者如果使用queryFn
属性,则为第一个参数。- 如果
query
没有参数,则必须显式提供void
类型。 - 如果
query
有一个可选参数,则必须提供一个包含参数类型和void
的联合类型,例如number | void
。
- 如果
- 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
},
}),
}),
})
queries
和mutations
也可以通过baseQuery
定义其返回类型,而不是上面显示的方法,但是,除非你希望所有查询和变异都返回相同的类型,否则建议将baseQuery
的返回类型保留为unknown
。
为queryFn
添加类型
如为查询和变异端点添加类型中所述,queryFn
将从提供给相应构建端点的泛型中接收其结果和参数类型。
- 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 }
},
}),
}),
})
queryFn
必须返回的错误类型由提供给createApi
的baseQuery
决定。
使用fetchBaseQuery
,错误类型如下所示
{
status: number
data: any
}
使用queryFn
和fetchBaseQuery
中的错误类型,上面的示例的错误情况可能如下所示
- 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 }
},
}),
}),
})
对于希望仅对每个端点使用queryFn
,而不包含baseQuery
的用户,RTK Query 提供了一个fakeBaseQuery
函数,该函数可用于轻松指定每个queryFn
应返回的错误类型。
- 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!' }
},
}),
}),
})
为dispatch
和getState
添加类型
createApi
在多个地方公开标准的 Redux dispatch
和getState
方法,例如生命周期方法中的lifecycleApi
参数,或传递给queryFn
方法和基本查询函数的baseQueryApi
参数。
通常,你的应用程序从商店设置中推断RootState
和AppDispatch
类型。由于createApi
必须在创建 Redux 商店之前调用,并且用作商店设置序列的一部分,因此它无法直接知道或使用这些类型 - 这会导致循环类型推断错误。
默认情况下,createApi
内部的dispatch
用法将被类型化为ThunkDispatch
,而getState
用法被类型化为() => unknown
。你需要在需要时断言类型 - getState() as RootState
。你也可以为函数包含一个显式返回类型,以打破循环类型推断循环
const api = createApi({
baseQuery,
endpoints: (build) => ({
getTodos: build.query<Todo[], void>({
async queryFn() {
// Cast state as `RootState`
const state = getState() as RootState
const text = state.todoTexts[queryFnCalls]
return { data: [{ id: `${queryFnCalls++}`, text }] }
},
}),
}),
})
为providesTags
/invalidatesTags
添加类型
RTK Query 利用缓存标签失效系统来提供自动重新获取过时数据。
使用函数表示法时,端点上的providesTags
和invalidatesTags
属性都将使用以下参数调用
- result:
ResultType
|undefined
- 成功查询返回的结果。该类型与提供给构建端点的ResultType
相对应。在查询的错误情况下,这将是undefined
。 - 错误:
ErrorType
|undefined
- 错误查询返回的错误。该类型与 提供给 api 的baseQuery
的Error
相对应。在查询的成功情况下,这将是undefined
。 - 参数:
QueryArg
- 当查询本身被调用时,提供给query
属性的参数。该类型与 提供给构建的端点的QueryArg
相对应。
当查询返回项目列表时,使用 providesTags
的推荐用例是使用实体 ID 为列表中的每个项目提供一个标签,以及一个 'LIST' ID 标签(参见 使用抽象标签 ID 的高级失效)。
这通常通过将接收到的数据映射到数组的结果进行扩展来完成,以及数组中用于 'LIST'
ID 标签的额外项。当扩展映射后的数组时,默认情况下,TypeScript 会将 type
属性扩展为 string
。由于标签 type
必须与提供给 api 的 tagTypes
属性的字符串文字之一相对应,因此宽泛的 string
类型将无法满足 TypeScript。为了缓解这种情况,标签 type
可以被强制转换为 as const
以防止类型被扩展为 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' }],
}),
}),
})
使用 TypeScript 和 skipToken
跳过查询
RTK Query 提供了使用 skip
参数作为查询钩子选项的一部分来有条件地跳过自动运行的查询的功能(参见 条件获取)。
TypeScript 用户可能会发现,当查询参数被类型化为非 undefined
,并且他们尝试在参数无效时 skip
查询时,他们会遇到无效类型场景。
- 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>
}
虽然你可能能够说服自己,除非 id
参数在当时是 number
,否则查询不会被调用,但 TypeScript 不会轻易被说服。
RTK Query 提供了一个 skipToken
导出,它可以作为 skip
选项的替代方案来跳过查询,同时保持类型安全。当 skipToken
作为查询参数传递给 useQuery
、useQueryState
或 useQuerySubscription
时,它提供与在查询选项中设置 skip: true
相同的效果,同时在 arg
可能在其他情况下为 undefined 的情况下也是有效的参数。
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>
}
类型安全的错误处理
当从 基本查询
中优雅地提供错误时,RTK 查询将直接提供错误。如果用户代码抛出的是意外错误而不是已处理的错误,该错误将被转换为 SerializedError
形状。用户应该确保在尝试访问错误的属性之前检查他们正在处理哪种类型的错误。这可以通过使用类型守卫以类型安全的方式完成,例如通过检查 区分属性,或使用 类型谓词。
当使用 fetchBaseQuery
作为你的基本查询时,错误将是 FetchBaseQueryError | SerializedError
类型。这些类型的具体形状可以在下面看到。
- 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 {}
错误结果示例
当使用 fetchBaseQuery
时,从钩子返回的 error
属性将具有 FetchBaseQueryError | SerializedError | undefined
类型。如果存在错误,你可以在将类型缩小为 FetchBaseQueryError
或 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) {
// 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>
)
}
// 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
}
内联错误处理示例
在 解包
突变调用后内联处理错误时,对于低于 4.4 版本的 typescript,抛出的错误将具有 any
类型,或者 对于 4.4+ 版本的 unknown
。为了安全地访问错误的属性,你必须首先将类型缩小为已知类型。这可以使用 类型谓词 完成,如下所示。
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>
)
}