import type { PatchCollection, Recipe } from '@internal/query/core/buildThunks' import type { ThunkDispatch, UnknownAction } from '@reduxjs/toolkit' import type { FetchBaseQueryError, FetchBaseQueryMeta, RootState, TypedMutationOnQueryStarted, TypedQueryOnQueryStarted, } from '@reduxjs/toolkit/query' import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query' const api = createApi({ baseQuery: fetchBaseQuery({ baseUrl: 'https://example.com' }), endpoints: () => ({}), }) describe('type tests', () => { test(`mutation: onStart and onSuccess`, async () => { const extended = api.injectEndpoints({ overrideExisting: true, endpoints: (build) => ({ injected: build.mutation({ query: () => '/success', async onQueryStarted(arg, { queryFulfilled }) { // awaiting without catching like this would result in an `unhandledRejection` exception if there was an error // unfortunately we cannot test for that in jest. const result = await queryFulfilled expectTypeOf(result).toMatchTypeOf<{ data: number meta?: FetchBaseQueryMeta }>() }, }), }), }) }) test('query types', () => { const extended = api.injectEndpoints({ overrideExisting: true, endpoints: (build) => ({ injected: build.query({ query: () => '/success', async onQueryStarted(arg, { queryFulfilled }) { queryFulfilled.then( (result) => { expectTypeOf(result).toMatchTypeOf<{ data: number meta?: FetchBaseQueryMeta }>() }, (reason) => { if (reason.isUnhandledError) { expectTypeOf(reason).toEqualTypeOf<{ error: unknown meta?: undefined isUnhandledError: true }>() } else { expectTypeOf(reason).toEqualTypeOf<{ error: FetchBaseQueryError isUnhandledError: false meta: FetchBaseQueryMeta | undefined }>() } }, ) queryFulfilled.catch((reason) => { if (reason.isUnhandledError) { expectTypeOf(reason).toEqualTypeOf<{ error: unknown meta?: undefined isUnhandledError: true }>() } else { expectTypeOf(reason).toEqualTypeOf<{ error: FetchBaseQueryError isUnhandledError: false meta: FetchBaseQueryMeta | undefined }>() } }) const result = await queryFulfilled expectTypeOf(result).toMatchTypeOf<{ data: number meta?: FetchBaseQueryMeta }>() }, }), }), }) }) test('mutation types', () => { const extended = api.injectEndpoints({ overrideExisting: true, endpoints: (build) => ({ injected: build.query({ query: () => '/success', async onQueryStarted(arg, { queryFulfilled }) { queryFulfilled.then( (result) => { expectTypeOf(result).toMatchTypeOf<{ data: number meta?: FetchBaseQueryMeta }>() }, (reason) => { if (reason.isUnhandledError) { expectTypeOf(reason).toEqualTypeOf<{ error: unknown meta?: undefined isUnhandledError: true }>() } else { expectTypeOf(reason).toEqualTypeOf<{ error: FetchBaseQueryError isUnhandledError: false meta: FetchBaseQueryMeta | undefined }>() } }, ) queryFulfilled.catch((reason) => { if (reason.isUnhandledError) { expectTypeOf(reason).toEqualTypeOf<{ error: unknown meta?: undefined isUnhandledError: true }>() } else { expectTypeOf(reason).toEqualTypeOf<{ error: FetchBaseQueryError isUnhandledError: false meta: FetchBaseQueryMeta | undefined }>() } }) const result = await queryFulfilled expectTypeOf(result).toMatchTypeOf<{ data: number meta?: FetchBaseQueryMeta }>() }, }), }), }) }) describe('typed `onQueryStarted` function', () => { test('TypedQueryOnQueryStarted creates a pre-typed version of onQueryStarted', () => { type Post = { id: number title: string userId: number } type PostsApiResponse = { posts: Post[] total: number skip: number limit: number } type QueryArgument = number | undefined type BaseQueryFunction = ReturnType const baseApiSlice = createApi({ baseQuery: fetchBaseQuery({ baseUrl: 'https://dummyjson.com' }), reducerPath: 'postsApi', tagTypes: ['Posts'], endpoints: (builder) => ({ getPosts: builder.query({ query: () => `/posts`, }), getPostById: builder.query({ query: (postId) => `/posts/${postId}`, }), }), }) const updatePostOnFulfilled: TypedQueryOnQueryStarted< PostsApiResponse, QueryArgument, BaseQueryFunction, 'postsApi' > = async (queryArgument, queryLifeCycleApi) => { const { dispatch, extra, getCacheEntry, getState, queryFulfilled, requestId, updateCachedData, } = queryLifeCycleApi expectTypeOf(queryArgument).toEqualTypeOf() expectTypeOf(dispatch).toEqualTypeOf< ThunkDispatch >() expectTypeOf(extra).toBeUnknown() expectTypeOf(getState).toEqualTypeOf< () => RootState >() expectTypeOf(requestId).toBeString() expectTypeOf(getCacheEntry).toBeFunction() expectTypeOf(updateCachedData).toEqualTypeOf< (updateRecipe: Recipe) => PatchCollection >() expectTypeOf(queryFulfilled).resolves.toEqualTypeOf<{ data: PostsApiResponse meta: FetchBaseQueryMeta | undefined }>() const result = await queryFulfilled const { posts } = result.data dispatch( baseApiSlice.util.upsertQueryEntries( posts.map((post) => ({ // Without `as const` this will result in a TS error in TS 4.7. endpointName: 'getPostById' as const, arg: post.id, value: post, })), ), ) } const extendedApiSlice = baseApiSlice.injectEndpoints({ endpoints: (builder) => ({ getPostsByUserId: builder.query({ query: (userId) => `/posts/user/${userId}`, onQueryStarted: updatePostOnFulfilled, }), }), }) }) test('TypedMutationOnQueryStarted creates a pre-typed version of onQueryStarted', () => { type Post = { id: number title: string userId: number } type PostsApiResponse = { posts: Post[] total: number skip: number limit: number } type QueryArgument = Pick & Partial type BaseQueryFunction = ReturnType const baseApiSlice = createApi({ baseQuery: fetchBaseQuery({ baseUrl: 'https://dummyjson.com' }), reducerPath: 'postsApi', tagTypes: ['Posts'], endpoints: (builder) => ({ getPosts: builder.query({ query: () => `/posts`, }), getPostById: builder.query({ query: (postId) => `/posts/${postId}`, }), }), }) const updatePostOnFulfilled: TypedMutationOnQueryStarted< Post, QueryArgument, BaseQueryFunction, 'postsApi' > = async (queryArgument, mutationLifeCycleApi) => { const { id, ...patch } = queryArgument const { dispatch, extra, getCacheEntry, getState, queryFulfilled, requestId, } = mutationLifeCycleApi const patchCollection = dispatch( baseApiSlice.util.updateQueryData('getPostById', id, (draftPost) => { Object.assign(draftPost, patch) }), ) expectTypeOf(queryFulfilled).resolves.toEqualTypeOf<{ data: Post meta: FetchBaseQueryMeta | undefined }>() expectTypeOf(queryArgument).toEqualTypeOf() expectTypeOf(dispatch).toEqualTypeOf< ThunkDispatch >() expectTypeOf(extra).toBeUnknown() expectTypeOf(getState).toEqualTypeOf< () => RootState >() expectTypeOf(requestId).toBeString() expectTypeOf(getCacheEntry).toBeFunction() expectTypeOf(mutationLifeCycleApi).not.toHaveProperty( 'updateCachedData', ) try { await queryFulfilled } catch { patchCollection.undo() } } const extendedApiSlice = baseApiSlice.injectEndpoints({ endpoints: (builder) => ({ addPost: builder.mutation>({ query: (body) => ({ url: `posts/add`, method: 'POST', body, }), onQueryStarted: updatePostOnFulfilled, }), updatePost: builder.mutation({ query: ({ id, ...patch }) => ({ url: `post/${id}`, method: 'PATCH', body: patch, }), onQueryStarted: updatePostOnFulfilled, }), }), }) }) }) })