import type { QueryStateSelector, UseMutation, UseQuery, } from '@internal/query/react/buildHooks' import { ANY } from '@internal/tests/utils/helpers' import type { SerializedError } from '@reduxjs/toolkit' import type { QueryDefinition, SubscriptionOptions, TypedQueryStateSelector, } from '@reduxjs/toolkit/query/react' import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react' import { useState } from 'react' let amount = 0 let nextItemId = 0 interface Item { id: number } const api = createApi({ baseQuery: (arg: any) => { if (arg?.body && 'amount' in arg.body) { amount += 1 } if (arg?.body && 'forceError' in arg.body) { return { error: { status: 500, data: null, }, } } if (arg?.body && 'listItems' in arg.body) { const items: Item[] = [] for (let i = 0; i < 3; i++) { const item = { id: nextItemId++ } items.push(item) } return { data: items } } return { data: arg?.body ? { ...arg.body, ...(amount ? { amount } : {}) } : {}, } }, endpoints: (build) => ({ getUser: build.query<{ name: string }, number>({ query: () => ({ body: { name: 'Timmy' }, }), }), getUserAndForceError: build.query<{ name: string }, number>({ query: () => ({ body: { forceError: true, }, }), }), getIncrementedAmount: build.query<{ amount: number }, void>({ query: () => ({ url: '', body: { amount, }, }), }), updateUser: build.mutation<{ name: string }, { name: string }>({ query: (update) => ({ body: update }), }), getError: build.query({ query: () => '/error', }), listItems: build.query({ serializeQueryArgs: ({ endpointName }) => { return endpointName }, query: ({ pageNumber }) => ({ url: `items?limit=1&offset=${pageNumber}`, body: { listItems: true, }, }), merge: (currentCache, newItems) => { currentCache.push(...newItems) }, forceRefetch: () => { return true }, }), }), }) describe('type tests', () => { test('useLazyQuery hook callback returns various properties to handle the result', () => { function User() { const [getUser] = api.endpoints.getUser.useLazyQuery() const [{ successMsg, errMsg, isAborted }, setValues] = useState({ successMsg: '', errMsg: '', isAborted: false, }) const handleClick = (abort: boolean) => async () => { const res = getUser(1) // no-op simply for clearer type assertions res.then((result) => { if (result.isSuccess) { expectTypeOf(result).toMatchTypeOf<{ data: { name: string } }>() } if (result.isError) { expectTypeOf(result).toMatchTypeOf<{ error: { status: number; data: unknown } | SerializedError }>() } }) expectTypeOf(res.arg).toBeNumber() expectTypeOf(res.requestId).toBeString() expectTypeOf(res.abort).toEqualTypeOf<() => void>() expectTypeOf(res.unsubscribe).toEqualTypeOf<() => void>() expectTypeOf(res.updateSubscriptionOptions).toEqualTypeOf< (options: SubscriptionOptions) => void >() expectTypeOf(res.refetch).toMatchTypeOf<() => void>() expectTypeOf(res.unwrap()).resolves.toEqualTypeOf<{ name: string }>() } return (
{successMsg}
{errMsg}
{isAborted ? 'Request was aborted' : ''}
) } }) test('useMutation hook callback returns various properties to handle the result', async () => { function User() { const [updateUser] = api.endpoints.updateUser.useMutation() const [successMsg, setSuccessMsg] = useState('') const [errMsg, setErrMsg] = useState('') const [isAborted, setIsAborted] = useState(false) const handleClick = async () => { const res = updateUser({ name: 'Banana' }) expectTypeOf(res).resolves.toMatchTypeOf< | { error: { status: number; data: unknown } | SerializedError } | { data: { name: string } } >() expectTypeOf(res.arg).toMatchTypeOf<{ endpointName: string originalArgs: { name: string } track?: boolean }>() expectTypeOf(res.requestId).toBeString() expectTypeOf(res.abort).toEqualTypeOf<() => void>() expectTypeOf(res.unwrap()).resolves.toEqualTypeOf<{ name: string }>() expectTypeOf(res.reset).toEqualTypeOf<() => void>() } return (
{successMsg}
{errMsg}
{isAborted ? 'Request was aborted' : ''}
) } }) test('top level named hooks', () => { interface Post { id: number name: string fetched_at: string } type PostsResponse = Post[] const api = createApi({ baseQuery: fetchBaseQuery({ baseUrl: 'https://example.com/' }), tagTypes: ['Posts'], endpoints: (build) => ({ getPosts: build.query({ query: () => ({ url: 'posts' }), providesTags: (result) => result ? result.map(({ id }) => ({ type: 'Posts', id })) : [], }), updatePost: build.mutation>({ query: ({ id, ...body }) => ({ url: `post/${id}`, method: 'PUT', body, }), invalidatesTags: (result, error, { id }) => [{ type: 'Posts', id }], }), addPost: build.mutation>({ query: (body) => ({ url: `post`, method: 'POST', body, }), invalidatesTags: ['Posts'], }), }), }) expectTypeOf(api.useGetPostsQuery).toEqualTypeOf( api.endpoints.getPosts.useQuery, ) expectTypeOf(api.useUpdatePostMutation).toEqualTypeOf( api.endpoints.updatePost.useMutation, ) expectTypeOf(api.useAddPostMutation).toEqualTypeOf( api.endpoints.addPost.useMutation, ) }) test('UseQuery type can be used to recreate the hook type', () => { const fakeQuery = ANY as UseQuery< typeof api.endpoints.getUser.Types.QueryDefinition > expectTypeOf(fakeQuery).toEqualTypeOf(api.endpoints.getUser.useQuery) }) test('UseMutation type can be used to recreate the hook type', () => { const fakeMutation = ANY as UseMutation< typeof api.endpoints.updateUser.Types.MutationDefinition > expectTypeOf(fakeMutation).toEqualTypeOf( api.endpoints.updateUser.useMutation, ) }) test('TypedQueryStateSelector creates a pre-typed version of QueryStateSelector', () => { type Post = { id: number title: string } type PostsApiResponse = { posts: Post[] total: number skip: number limit: number } type QueryArgument = number | undefined type BaseQueryFunction = ReturnType type SelectedResult = Pick const postsApiSlice = createApi({ baseQuery: fetchBaseQuery({ baseUrl: 'https://dummyjson.com/posts' }), reducerPath: 'postsApi', tagTypes: ['Posts'], endpoints: (build) => ({ getPosts: build.query({ query: (limit = 5) => `?limit=${limit}&select=title`, }), }), }) const { useGetPostsQuery } = postsApiSlice function PostById({ id }: { id: number }) { const { post } = useGetPostsQuery(undefined, { selectFromResult: (state) => ({ post: state.data?.posts.find((post) => post.id === id), }), }) expectTypeOf(post).toEqualTypeOf() return
  • {post?.title}
  • } const EMPTY_ARRAY: Post[] = [] const typedSelectFromResult: TypedQueryStateSelector< PostsApiResponse, QueryArgument, BaseQueryFunction, SelectedResult > = (state) => ({ posts: state.data?.posts ?? EMPTY_ARRAY }) expectTypeOf< TypedQueryStateSelector< PostsApiResponse, QueryArgument, BaseQueryFunction, SelectedResult > >().toEqualTypeOf< QueryStateSelector< SelectedResult, QueryDefinition< QueryArgument, BaseQueryFunction, string, PostsApiResponse > > >() expectTypeOf(typedSelectFromResult).toEqualTypeOf< QueryStateSelector< SelectedResult, QueryDefinition< QueryArgument, BaseQueryFunction, string, PostsApiResponse > > >() function PostsList() { const { posts } = useGetPostsQuery(undefined, { selectFromResult: typedSelectFromResult, }) expectTypeOf(posts).toEqualTypeOf() return (
      {posts.map((post) => ( ))}
    ) } }) })