使用 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+。
- 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
可以使用 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
屬性可作為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,
},
}),
}),
})
輸入查詢和突變 端點
api 的 端點
定義為使用建構函數語法的物件。查詢
和 突變
端點都可以透過提供 <ResultType, QueryArg>
格式的泛型類型。
ResultType
- 查詢傳回的最終資料類型,考量到選用的transformResponse
。- 如果未提供
transformResponse
,則會將其視為成功的查詢將傳回此類型。 - 如果 提供
transformResponse
,則還必須指定transformResponse
的輸入類型,以指出初始查詢傳回的類型。transformResponse
的傳回類型必須與ResultType
相符。 - 如果使用
queryFn
而不是query
,則它必須傳回下列成功案例的形狀{
data: ResultType
}
- 如果未提供
QueryArg
- 將傳遞為端點的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
。 - error:
ErrorType
|undefined
- 發生錯誤的查詢回傳的錯誤。類型對應於 提供給 api 的baseQuery
的Error
。在查詢的成功案例中,這將會是undefined
。 - arg:
QueryArg
- 在呼叫查詢本身時提供給query
屬性的參數。類型對應於 提供給內建端點 的QueryArg
。
當查詢傳回項目清單時,建議使用 providesTags
的使用案例是使用實體 ID 為清單中的每個項目提供標籤,以及「LIST」ID 標籤(請參閱 使用抽象標籤 ID 進行進階無效化)。
這通常透過將接收資料的對應結果擴充成陣列來撰寫,以及在陣列中為 'LIST'
ID 標籤新增一個項目。在擴充對應的陣列時,預設情況下,TypeScript 會將 type
屬性擴充為 string
。由於標籤 type
必須與提供給 tagTypes
API 屬性的字串文字之一相符,因此廣泛的 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' }],
}),
}),
})
使用 skipToken
略過 TypeScript 中的查詢
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
可能未定義的其他情況下,它也會是一個有效的參數。
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
類型,或 unknown
對於 4.4+ 版本。為了安全地存取錯誤的屬性,您必須先將類型縮小為已知類型。這可以使用 類型謂詞 來完成,如下所示。
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>
)
}