跳到主要内容

 

与 TypeScript 一起使用

你将学到什么
  • 如何使用各种 RTK Query APIs 与 TypeScript

简介

与 Redux Toolkit 包的其余部分一样,RTK Query 是用 TypeScript 编写的,其 API 旨在在 TypeScript 应用程序中无缝使用。

此页面提供了使用 RTK Query 中包含的 APIs 与 TypeScript 以及如何正确地对它们进行类型化的详细信息。

信息

我们强烈建议使用 TypeScript 4.1+ 与 RTK Query 以获得最佳结果。

如果你遇到任何在此页面上未描述的类型问题,请开启一个问题进行讨论。

createApi

使用自动生成的 React 钩子

RTK Query 的 React 特定入口点导出了一个版本的 createApi,它会自动为定义的查询和突变 endpoints 生成 React 钩子。

要作为 TypeScript 用户使用自动生成的 React 钩子,你需要使用 TS4.1+

// 文件: src/services/types.ts noEmit
export type Pokemon = {}

// 文件: src/services/pokemon.ts
// 需要使用 React 特定的入口点以允许生成 React 钩子
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'
import type { Pokemon } from './types'

// 使用基础 URL 和预期的端点定义服务
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 const { useGetPokemonByNameQuery } = pokemonApi

对于旧版本的 TS,你可以使用 api.endpoints.[endpointName].useQuery/useMutation 来访问相同的钩子。

直接访问 api 钩子
// 文件: src/services/types.ts noEmit
export type Pokemon = {}

// 文件: src/services/pokemon.ts noEmit
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: (builder) => ({
getPokemonByName: builder.query<Pokemon, string>({
query: (name) => `pokemon/${name}`,
}),
}),
})

export const { useGetPokemonByNameQuery } = pokemonApi

// 文件: src/services/manual-query.ts
import { pokemonApi } from './pokemon'

const useGetPokemonByNameQuery = pokemonApi.endpoints.getPokemonByName.useQuery

baseQuery 进行类型化

可以使用 RTK Query 导出的 BaseQueryFn 类型对自定义 baseQuery 进行类型化。

Base 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
}

BaseQueryFn 类型接受以下泛型:

  • Args - 函数的第一个参数的类型。端点上的 query 属性返回的结果将在此处传递。
  • Result - 成功情况下 data 属性要返回的类型。除非你期望所有查询和突变都返回相同的类型,否则建议将此类型保持为 unknown,并如下所示单独指定类型。
  • Error - 错误情况下 error 属性要返回的类型。此类型也适用于 API 定义中所有端点使用的 queryFn 函数。
  • DefinitionExtraOptions - 函数的第三个参数的类型。在端点上提供给 extraOptions 属性的值将在此处传递。
  • Meta - 可能从调用 baseQuery 返回的 meta 属性的类型。meta 属性可以作为 transformResponsetransformErrorResponse 的第二个参数访问。
备注

baseQuery 返回的 meta 属性总是被视为可能未定义的,因为在错误情况下可能会导致它未被提供。在访问 meta 属性的值时,应考虑到这一点,例如使用可选链

简单的 baseQuery TypeScript 示例
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` 的类型为 `string`
// `api` 的类型为 `BaseQueryApi`(不可配置)
// `extraOptions` 的类型为 `{ 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 添加类型

使用构建器语法,endpoints 被定义为一个对象。通过向 <ResultType, QueryArg> 格式的泛型提供类型,可以为 querymutation endpoints 添加类型。

  • ResultType - 查询返回的最终数据的类型,考虑到可选的 transformResponse
    • 如果没有提供 transformResponse,那么就会被视为成功的查询将返回这种类型。
    • 如果提供了 transformResponse,那么 transformResponse 的输入类型也必须指定,以指示初始查询返回的类型。transformResponse 的返回类型必须与 ResultType 匹配。
    • 如果使用 queryFn 而不是 query,那么它必须返回以下形状的成功案例:
      {
      data: ResultType
      }
  • QueryArg - 将作为唯一参数传递给 endpoint 的 query 属性,或者如果使用 queryFn 属性,则作为第一个参数。
    • 如果 query 没有参数,那么必须明确提供 void 类型。
    • 如果 query 有一个可选参数,那么必须提供参数类型和 void 的联合类型,例如 number | void
使用 TypeScript 定义 endpoints
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>({
// 从 `QueryArg` 类型推断出 `number`
// v
query: (id) => `post/${id}`,
// 当使用 `transformResponse` 时,必须为查询返回的原始结果提供明确的类型
// v
transformResponse: (rawResult: { result: { post: Post } }, meta) => {
// ^
// 基于用于 `baseQuery` 的类型,可选的 `meta` 属性可用

// `transformResponse` 的返回值必须与 `ResultType` 匹配
return rawResult.result.post
},
}),
}),
})
备注

queriesmutations 的返回类型也可以由 baseQuery 定义,而不是上面显示的方法,但是,除非你期望所有的查询和变异都返回相同的类型,否则建议将 baseQuery 的返回类型保留为 unknown

queryFn 添加类型

如在 为查询和变异 endpoints 添加类型 中提到的,queryFn 将从提供给相应构建 endpoint 的泛型中接收其结果和参数类型。

// 文件: randomData.ts noEmit
export declare const getRandomName: () => string

// 文件: api.ts
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>({
// 从 `QueryArg` 类型推断出 `number`
// v
queryFn: (arg, queryApi, extraOptions, baseQuery) => {
const post: Post = {
id: arg,
name: getRandomName(),
}
// 对于成功的情况,`data` 属性的返回类型
// 必须与 `ResultType` 匹配
// v
return { data: post }
},
}),
}),
})

queryFn 必须返回的错误类型由提供给 createApibaseQuery 确定。

对于 fetchBaseQuery,错误类型如下:

fetchBaseQuery 错误形状
{
status: number
data: any
}

对于上面使用 queryFn 和来自 fetchBaseQuery 的错误类型的例子,错误情况可能如下:

queryFn 错误示例,错误类型来自 fetchBaseQuery
// 文件: randomData.ts noEmit
export declare const getRandomName: () => string

// 文件: api.ts
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 }
},
}),
}),
})

对于希望 为每个 endpoint 使用 queryFn 并且完全不包含 baseQuery 的用户,RTK Query 提供了一个 fakeBaseQuery 函数,可以用来轻松指定每个 queryFn 应返回的错误类型。

排除所有 endpoints 的 baseQuery
import { createApi, fakeBaseQuery } from '@reduxjs/toolkit/query'

type CustomErrorType = { reason: 'too cold' | 'too hot' }

const api = createApi({
// 这种类型将被用作所有 `queryFn` 函数提供的错误类型
// 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!' }
},
}),
}),
})

dispatchgetState 类型化

createApi 在多个地方暴露了标准的 Redux dispatchgetState 方法,例如在生命周期方法中的 lifecycleApi 参数,或者传递给 queryFn 方法和基础查询函数的 baseQueryApi 参数。

通常,你的应用程序会从存储设置中推断出 RootStateAppDispatch 类型。由于 createApi 必须在创建 Redux 存储之前被调用,并且被用作存储设置序列的一部分,因此它不能直接知道或使用这些类型 - 这将导致循环类型推断错误。

默认情况下,createApi 内部的 dispatch 使用将被类型化为 ThunkDispatchgetState 的使用被类型化为 () => unknown。你需要在需要的时候断言类型 - getState() as RootState。你也可以为函数包含一个明确的返回类型,以打破循环类型推断周期:

const api = createApi({
baseQuery,
endpoints: (build) => ({
getTodos: build.query<Todo[], void>({
async queryFn() {
// 将状态转换为 `RootState`
const state = getState() as RootState
const text = state.todoTexts[queryFnCalls]
return { data: [{ id: `${queryFnCalls++}`, text }] }
},
}),
}),
})

providesTags/invalidatesTags 类型化

RTK Query 使用缓存标签失效系统,以提供自动重新获取过期数据的功能。

当使用函数表示法时,端点上的 providesTagsinvalidatesTags 属性都会被以下参数调用:

当查询返回一个项目列表时,使用 providesTags 的推荐用例是为列表中的每个项目提供一个标签,使用实体 ID,以及一个 'LIST' ID 标签(参见使用抽象标签 ID 的高级失效)。

这通常通过将接收到的数据映射到数组的结果扩展,以及数组中的额外项用于 'LIST' ID 标签来编写。当扩展映射数组时,默认情况下,TypeScript 将 type 属性扩展为 string。由于标签 type 必须对应到提供给 api 的 tagTypes 属性的一个字符串字面量,所以广泛的 string 类型将不会满足 TypeScript。为了缓解这个问题,可以将标签 type 转换为 as const,以防止类型被扩展为 string

providesTags TypeScript 示例
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' }],
}),
}),
})

使用 skipToken 在 TypeScript 中跳过查询

RTK Query 提供了使用 skip 参数作为查询钩子选项的部分,以有条件地跳过自动运行的查询的能力(参见条件获取)。

TypeScript 用户可能会发现,当查询参数被类型化为不是 undefined,并且他们试图在参数无效时 skip 查询时,他们会遇到无效的类型场景。

API 定义
// 文件: types.ts noEmit
export interface Post {
id: number
name: string
}

// 文件: api.ts
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'
import type { Post } from './types'

export const api = createApi({
baseQuery: fetchBaseQuery({ baseUrl: '/' }),
endpoints: (build) => ({
// 查询参数必须是 `number`,不能是 `undefined`
// V
getPost: build.query<Post, number>({
query: (id) => `post/${id}`,
}),
}),
})

export const { useGetPostQuery } = api
在组件中使用 skip
import { useGetPostQuery } from './api'

function MaybePost({ id }: { id?: number }) {
// 这将产生一个 TypeScript 错误:
// 类型 'number | undefined' 的参数不能赋值给类型 'number | unique symbol' 的参数。
// 类型 'undefined' 不能赋值给类型 'number | unique symbol'。

// @ts-expect-error 传递的 id 必须是一个数字,但是我们在它不是数字时不调用它
const { data } = useGetPostQuery(id, { skip: !id })

return <div>...</div>
}

虽然你可能能够说服自己,除非 id 参数在那时是一个 number,否则查询不会被调用,但 TypeScript 不会那么容易被说服。

RTK Query 提供了一个 skipToken 导出,它可以作为跳过查询的替代方法,同时保持类型安全。当 skipToken 作为查询参数传递给 useQueryuseQueryStateuseQuerySubscription 时,它提供了与在查询选项中设置 skip: true 相同的效果,同时在 arg 可能否则未定义的情况下也是一个有效的参数。

在组件中使用 skipToken
import { skipToken } from '@reduxjs/toolkit/query/react'
import { useGetPostQuery } from './api'

function MaybePost({ id }: { id?: number }) {
// 当 `id` 是 nullish,我们仍然会跳过查询。
// TypeScript 也很高兴现在查询只会被一个 `number` 调用
const { data } = useGetPostQuery(id ?? skipToken)

return <div>...</div>
}

类型安全的错误处理

当一个错误从 base query 中优雅地提供时,RTK 查询将直接提供错误。如果用户代码抛出了一个意外的错误,而不是一个处理过的错误,那么这个错误将被转换为 SerializedError 形状。用户应确保在尝试访问其属性之前,检查他们正在处理哪种类型的错误。这可以通过使用类型保护,例如,通过检查鉴别属性,或者使用类型谓词来以类型安全的方式完成。

当使用 fetchBaseQuery 作为你的基础查询时,错误将是 FetchBaseQueryError | SerializedError 类型。这些类型的具体形状可以在下面看到。

FetchBaseQueryError 类型
export type FetchBaseQueryError =
| {
/**
* * `number`:
* HTTP 状态码
*/
status: number
data: unknown
}
| {
/**
* * `"FETCH_ERROR"`:
* 在执行 `fetch` 或 `fetchFn` 回调选项期间发生的错误
**/
status: 'FETCH_ERROR'
data?: undefined
error: string
}
| {
/**
* * `"PARSING_ERROR"`:
* 在解析过程中发生的错误。
* 最有可能的是返回了一个非 JSON 响应,使用默认的 `responseHandler` "JSON",
* 或者在执行自定义 `responseHandler` 时发生了错误。
**/
status: 'PARSING_ERROR'
originalStatus: number
data: string
error: string
}
| {
/**
* * `"CUSTOM_ERROR"`:
* 你可以从你的 `queryFn` 返回的自定义错误类型,其中其他错误可能没有意义。
**/
status: 'CUSTOM_ERROR'
data?: unknown
error: string
}
SerializedError 类型
export interface SerializedError {
name?: string
message?: string
stack?: string
code?: string
}

错误结果示例

当使用 fetchBaseQuery 时,从钩子返回的 error 属性将具有 FetchBaseQueryError | SerializedError | undefined 类型。如果存在错误,你可以在将类型缩小到 FetchBaseQueryErrorSerializedError 后访问错误属性。

import { api } from './services/api'

function PostDetail() {
const { data, error, isLoading } = usePostsQuery()

if (isLoading) {
return <div>加载中...</div>
}

if (error) {
if ('status' in error) {
// 你可以在这里访问 `FetchBaseQueryError` 的所有属性
const errMsg = 'error' in error ? error.error : JSON.stringify(error.data)

return (
<div>
<div>发生了一个错误:</div>
<div>{errMsg}</div>
</div>
)
} else {
// 你可以在这里访问 `SerializedError` 的所有属性
return <div>{error.message}</div>
}
}

if (data) {
return (
<div>
{data.map((post) => (
<div key={post.id}>名称: {post.name}</div>
))}
</div>
)
}

return null
}

内联错误处理示例

当在 unwrapping 一个 mutation 调用后内联处理错误时,抛出的错误将对于 TypeScript 版本低于 4.4 的类型为 any,或者对于 4.4+ 版本为 unknown。为了安全地访问错误的属性,你必须首先将类型缩小到已知类型。这可以使用下面显示的 类型谓词 来完成。

services/helpers.ts
import { FetchBaseQueryError } from '@reduxjs/toolkit/query'

/**
* 类型谓词,用于将未知错误缩小为 `FetchBaseQueryError`
*/
export function isFetchBaseQueryError(
error: unknown,
): error is FetchBaseQueryError {
return typeof error === 'object' && error != null && 'status' in error
}

/**
* 类型谓词,用于将未知错误缩小为具有字符串 'message' 属性的对象
*/
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)) {
// 你可以在这里访问 `FetchBaseQueryError` 的所有属性
const errMsg = 'error' in err ? err.error : JSON.stringify(err.data)
enqueueSnackbar(errMsg, { variant: 'error' })
} else if (isErrorWithMessage(err)) {
// 你可以在这里访问字符串 'message' 属性
enqueueSnackbar(err.message, { variant: 'error' })
}
}
}

return (
<div>
<input value={name} onChange={(e) => setName(e.target.value)} />
<button>添加帖子</button>
</div>
)
}