TypeScript: Instructions on how to use RTK Query with TypeScript">TypeScript: Instructions on how to use RTK Query with TypeScript">
跳至主要內容

使用 TypeScript

您將學到
  • 有關如何將各種 RTK Query API 與 TypeScript 搭配使用的詳細資訊

簡介

與 Redux Toolkit 套件的其他部分一樣,RTK Query 是以 TypeScript 編寫的,其 API 旨在讓 TypeScript 應用程式能無縫使用。

此頁面提供有關如何將 RTK Query 中包含的 API 與 TypeScript 搭配使用,以及如何正確地為其設定類型的詳細資訊。

資訊

我們強烈建議將 RTK Query 與 TypeScript 4.1+ 搭配使用,以獲得最佳效果。

如果您遇到此頁面未說明的類型問題,請 開啟議題 進行討論。

createApi

使用自動產生的 React Hooks

RTK Query 的 React 專用進入點匯出 createApi 的版本,可自動為每個已定義的查詢和突變 endpoints 產生 React hooks。

要將自動產生的 React Hooks 用於 TypeScript 使用者,您需要使用 TS4.1+

// 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

對於舊版本的 TS,您可以使用 api.endpoints.[endpointName].useQuery/useMutation 來存取相同的 hooks。

直接存取 api hooks
import { pokemonApi } from './pokemon'

const useGetPokemonByNameQuery = pokemonApi.endpoints.getPokemonByName.useQuery

輸入 baseQuery

可以使用 RTK Query 匯出的 BaseQueryFn 類型來輸入自訂 baseQuery

基本查詢簽章
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 屬性將始終被視為潛在未定義,因為錯誤情況下的 throw 可能導致未提供它。存取 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` 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,
},
}),
}),
})

輸入查詢和突變 端點

api 的 端點定義為使用建構函數語法的物件。查詢突變端點都可以透過提供 <ResultType, QueryArg> 格式的泛型類型。

  • ResultType - 查詢傳回的最終資料類型,考量到選用的 transformResponse
    • 如果未提供 transformResponse,則會將其視為成功的查詢將傳回此類型。
    • 如果 提供 transformResponse,則還必須指定 transformResponse 的輸入類型,以指出初始查詢傳回的類型。transformResponse 的傳回類型必須與 ResultType 相符。
    • 如果使用 queryFn 而不是 query,則它必須傳回下列成功案例的形狀
      {
      data: ResultType
      }
  • QueryArg - 將傳遞為端點的 query 屬性的唯一參數的輸入類型,或如果使用,則為 queryFn 屬性的第一個參數。
    • 如果 query 沒有參數,則必須明確提供 void 類型。
    • 如果 query 有選用參數,則必須提供具有參數類型和 void 的聯合類型,例如 number | void
使用 TypeScript 定義端點
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
},
}),
}),
})
註解

queriesmutations 也可以透過 baseQuery 定義其傳回類型,而不是上述方法,但是,除非您預期所有查詢和突變傳回相同的類型,否則建議將 baseQuery 的傳回類型保留為 unknown

輸入 queryFn

輸入查詢和突變端點 所述,queryFn 將從提供給對應建置端點的泛型中接收其結果和參數類型。

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 }
},
}),
}),
})

queryFn 必須回傳的錯誤類型是由提供給 createApibaseQuery 所決定的。

使用 fetchBaseQuery 時,錯誤類型如下

fetchBaseQuery 錯誤形狀
{
status: number
data: any
}

使用 queryFnfetchBaseQuery 的錯誤類型的範例錯誤案例如下

使用 fetchBaseQuery 的錯誤類型,queryFn 錯誤範例
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 而完全不包含 baseQuery 的使用者,RTK Query 提供了一個 fakeBaseQuery 函式,可輕鬆指定每個 queryFn 應回傳的錯誤類型。

排除所有端點的 baseQuery
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!' }
},
}),
}),
})

輸入 dispatchgetState

createApi 在多個地方公開標準 Redux dispatchgetState 方法,例如生命週期方法中的 lifecycleApi 參數,或傳遞給 queryFn 方法和基本查詢函式的 baseQueryApi 參數。

通常,您的應用程式會從儲存設定中推斷 RootStateAppDispatch 類型。由於 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 利用快取標籤失效系統來提供 過期資料的自動重新擷取

使用函式表示法時,端點上的 providesTagsinvalidatesTags 屬性會使用下列參數呼叫

  • result: ResultType | undefined - 成功查詢回傳的結果。類型對應於 提供給內建端點ResultType。在查詢的錯誤案例中,這將會是 undefined
  • error: ErrorType | undefined - 發生錯誤的查詢回傳的錯誤。類型對應於 提供給 api 的 baseQueryError。在查詢的成功案例中,這將會是 undefined
  • arg: QueryArg - 在呼叫查詢本身時提供給 query 屬性的參數。類型對應於 提供給內建端點QueryArg

當查詢傳回項目清單時,建議使用 providesTags 的使用案例是使用實體 ID 為清單中的每個項目提供標籤,以及「LIST」ID 標籤(請參閱 使用抽象標籤 ID 進行進階無效化)。

這通常透過將接收資料的對應結果擴充成陣列來撰寫,以及在陣列中為 'LIST' ID 標籤新增一個項目。在擴充對應的陣列時,預設情況下,TypeScript 會將 type 屬性擴充為 string。由於標籤 type 必須與提供給 tagTypes API 屬性的字串文字之一相符,因此廣泛的 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 定義
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
在元件中使用 skip
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 傳遞為 useQueryuseQueryStateuseQuerySubscription 的查詢參數時,它會提供與在查詢選項中設定 skip: true 相同的效果,同時在 arg 可能未定義的其他情況下,它也會是一個有效的參數。

在元件中使用 skipToken
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。這些類型的具體形狀如下所示。

FetchBaseQueryError 類型
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
}
SerializedError 類型
export interface SerializedError {
name?: string
message?: string
stack?: string
code?: string
}

錯誤結果範例

使用 fetchBaseQuery 時,從掛勾傳回的 error 屬性將具有類型 FetchBaseQueryError | SerializedError | undefined。如果存在錯誤,您可以在將類型縮小為 FetchBaseQueryErrorSerializedError 後存取錯誤屬性。

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 類型,或 unknown 對於 4.4+ 版本。為了安全地存取錯誤的屬性,您必須先將類型縮小為已知類型。這可以使用 類型謂詞 來完成,如下所示。

services/helpers.ts
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'
)
}
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)) {
// 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>
)
}