UNPKG

@reduxjs/toolkit

Version:

The official, opinionated, batteries-included toolset for efficient Redux development

517 lines (469 loc) 16.8 kB
import { setupApiStore } from '@internal/tests/utils/helpers' import type { EntityState, SerializedError } from '@reduxjs/toolkit' import { configureStore, createEntityAdapter } from '@reduxjs/toolkit' import type { DefinitionsFromApi, FetchBaseQueryError, FetchBaseQueryMeta, MutationDefinition, OverrideResultType, QueryDefinition, TagDescription, TagTypesFromApi, } from '@reduxjs/toolkit/query' import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query' import * as v from 'valibot' import type { Post } from './mocks/handlers' describe('type tests', () => { test('sensible defaults', () => { const api = createApi({ baseQuery: fetchBaseQuery(), endpoints: (build) => ({ getUser: build.query<unknown, void>({ query(id) { return { url: `user/${id}` } }, }), updateUser: build.mutation<unknown, void>({ query: () => '', }), }), }) configureStore({ reducer: { [api.reducerPath]: api.reducer, }, middleware: (gDM) => gDM().concat(api.middleware), }) expectTypeOf(api.reducerPath).toEqualTypeOf<'api'>() expectTypeOf(api.util.invalidateTags) .parameter(0) .toEqualTypeOf<(null | undefined | TagDescription<never>)[]>() }) describe('endpoint definition typings', () => { const api = createApi({ baseQuery: (from: 'From'): { data: 'To' } | Promise<{ data: 'To' }> => ({ data: 'To', }), endpoints: () => ({}), tagTypes: ['typeA', 'typeB'], }) test('query: query & transformResponse types', () => { api.injectEndpoints({ endpoints: (build) => ({ query: build.query<'RetVal', 'Arg'>({ query: (x: 'Arg') => 'From' as const, transformResponse(r: 'To') { return 'RetVal' as const }, }), query1: build.query<'RetVal', 'Arg'>({ // @ts-expect-error query: (x: 'Error') => 'From' as const, transformResponse(r: 'To') { return 'RetVal' as const }, }), query2: build.query<'RetVal', 'Arg'>({ // @ts-expect-error query: (x: 'Arg') => 'Error' as const, transformResponse(r: 'To') { return 'RetVal' as const }, }), query3: build.query<'RetVal', 'Arg'>({ query: (x: 'Arg') => 'From' as const, // @ts-expect-error transformResponse(r: 'Error') { return 'RetVal' as const }, }), query4: build.query<'RetVal', 'Arg'>({ query: (x: 'Arg') => 'From' as const, // @ts-expect-error transformResponse(r: 'To') { return 'Error' as const }, }), queryInference1: build.query<'RetVal', 'Arg'>({ query: (x) => { expectTypeOf(x).toEqualTypeOf<'Arg'>() return 'From' }, transformResponse(r) { expectTypeOf(r).toEqualTypeOf<'To'>() return 'RetVal' }, }), queryInference2: (() => { const query = build.query({ query: (x: 'Arg') => 'From' as const, transformResponse(r: 'To') { return 'RetVal' as const }, }) expectTypeOf(query).toMatchTypeOf< QueryDefinition<'Arg', any, any, 'RetVal'> >() return query })(), }), }) }) test('mutation: query & transformResponse types', () => { api.injectEndpoints({ endpoints: (build) => ({ query: build.mutation<'RetVal', 'Arg'>({ query: (x: 'Arg') => 'From' as const, transformResponse(r: 'To') { return 'RetVal' as const }, }), query1: build.mutation<'RetVal', 'Arg'>({ // @ts-expect-error query: (x: 'Error') => 'From' as const, transformResponse(r: 'To') { return 'RetVal' as const }, }), query2: build.mutation<'RetVal', 'Arg'>({ // @ts-expect-error query: (x: 'Arg') => 'Error' as const, transformResponse(r: 'To') { return 'RetVal' as const }, }), query3: build.mutation<'RetVal', 'Arg'>({ query: (x: 'Arg') => 'From' as const, // @ts-expect-error transformResponse(r: 'Error') { return 'RetVal' as const }, }), query4: build.mutation<'RetVal', 'Arg'>({ query: (x: 'Arg') => 'From' as const, // @ts-expect-error transformResponse(r: 'To') { return 'Error' as const }, }), mutationInference1: build.mutation<'RetVal', 'Arg'>({ query: (x) => { expectTypeOf(x).toEqualTypeOf<'Arg'>() return 'From' }, transformResponse(r) { expectTypeOf(r).toEqualTypeOf<'To'>() return 'RetVal' }, }), mutationInference2: (() => { const query = build.mutation({ query: (x: 'Arg') => 'From' as const, transformResponse(r: 'To') { return 'RetVal' as const }, }) expectTypeOf(query).toMatchTypeOf< MutationDefinition<'Arg', any, any, 'RetVal'> >() return query })(), }), }) }) describe('enhancing endpoint definitions', () => { const baseQuery = (x: string) => ({ data: 'success' }) function getNewApi() { return createApi({ baseQuery, tagTypes: ['old'], endpoints: (build) => ({ query1: build.query<'out1', 'in1'>({ query: (id) => `${id}` }), query2: build.query<'out2', 'in2'>({ query: (id) => `${id}` }), mutation1: build.mutation<'out1', 'in1'>({ query: (id) => `${id}`, }), mutation2: build.mutation<'out2', 'in2'>({ query: (id) => `${id}`, }), }), }) } const api1 = getNewApi() test('warn on wrong tagType', () => { const storeRef = setupApiStore(api1, undefined, { withoutTestLifecycles: true, }) api1.enhanceEndpoints({ endpoints: { query1: { // @ts-expect-error providesTags: ['new'], }, query2: { // @ts-expect-error providesTags: ['missing'], }, }, }) const enhanced = api1.enhanceEndpoints({ addTagTypes: ['new'], endpoints: { query1: { providesTags: ['new'], }, query2: { // @ts-expect-error providesTags: ['missing'], }, }, }) storeRef.store.dispatch(api1.endpoints.query1.initiate('in1')) storeRef.store.dispatch(api1.endpoints.query2.initiate('in2')) enhanced.enhanceEndpoints({ endpoints: { query1: { // returned `enhanced` api contains "new" entityType providesTags: ['new'], }, query2: { // @ts-expect-error providesTags: ['missing'], }, }, }) }) test('modify', () => { const storeRef = setupApiStore(api1, undefined, { withoutTestLifecycles: true, }) api1.enhanceEndpoints({ endpoints: { query1: { query: (x) => { expectTypeOf(x).toEqualTypeOf<'in1'>() return 'modified1' }, }, query2(definition) { definition.query = (x) => { expectTypeOf(x).toEqualTypeOf<'in2'>() return 'modified2' } }, mutation1: { query: (x) => { expectTypeOf(x).toEqualTypeOf<'in1'>() return 'modified1' }, }, mutation2(definition) { definition.query = (x) => { expectTypeOf(x).toEqualTypeOf<'in2'>() return 'modified2' } }, // @ts-expect-error nonExisting: {}, }, }) storeRef.store.dispatch(api1.endpoints.query1.initiate('in1')) storeRef.store.dispatch(api1.endpoints.query2.initiate('in2')) storeRef.store.dispatch(api1.endpoints.mutation1.initiate('in1')) storeRef.store.dispatch(api1.endpoints.mutation2.initiate('in2')) }) test('updated transform response types', async () => { const baseApi = createApi({ baseQuery: fetchBaseQuery({ baseUrl: 'https://example.com' }), tagTypes: ['old'], endpoints: (build) => ({ query1: build.query<'out1', void>({ query: () => 'success' }), mutation1: build.mutation<'out1', void>({ query: () => 'success' }), }), }) type Transformed = { value: string } type Definitions = DefinitionsFromApi<typeof api1> type TagTypes = TagTypesFromApi<typeof api1> type Q1Definition = OverrideResultType< Definitions['query1'], Transformed > type M1Definition = OverrideResultType< Definitions['mutation1'], Transformed > type UpdatedDefinitions = Omit<Definitions, 'query1' | 'mutation1'> & { query1: Q1Definition mutation1: M1Definition } const enhancedApi = baseApi.enhanceEndpoints< TagTypes, UpdatedDefinitions >({ endpoints: { query1: { transformResponse: (a, b, c) => ({ value: 'transformed', }), }, mutation1: { transformResponse: (a, b, c) => ({ value: 'transformed', }), }, }, }) const storeRef = setupApiStore(enhancedApi, undefined, { withoutTestLifecycles: true, }) const queryResponse = await storeRef.store.dispatch( enhancedApi.endpoints.query1.initiate(), ) expectTypeOf(queryResponse.data).toMatchTypeOf< Transformed | undefined >() const mutationResponse = await storeRef.store.dispatch( enhancedApi.endpoints.mutation1.initiate(), ) expectTypeOf(mutationResponse).toMatchTypeOf< | { data: Transformed } | { error: FetchBaseQueryError | SerializedError } >() }) }) describe('endpoint schemas', () => { const argSchema = v.object({ id: v.number() }) const postSchema = v.object({ id: v.number(), title: v.string(), body: v.string(), }) satisfies v.GenericSchema<Post> const errorResponseSchema = v.object({ status: v.number(), data: v.unknown(), }) satisfies v.GenericSchema<FetchBaseQueryError> const metaSchema = v.object({ request: v.instance(Request), response: v.optional(v.instance(Response)), }) satisfies v.GenericSchema<FetchBaseQueryMeta> test('schemas must match', () => { createApi({ baseQuery: fetchBaseQuery({ baseUrl: 'https://example.com' }), endpoints: (build) => ({ query: build.query<Post, { id: number }>({ query: ({ id }) => `/post/${id}`, argSchema, responseSchema: postSchema, errorResponseSchema, metaSchema, }), bothMismatch: build.query<Post, { id: number }>({ query: ({ id }) => `/post/${id}`, // @ts-expect-error wrong schema argSchema: v.object({ id: v.string() }), // @ts-expect-error wrong schema responseSchema: v.object({ id: v.string() }), // @ts-expect-error wrong schema errorResponseSchema: v.object({ status: v.string() }), // @ts-expect-error wrong schema metaSchema: v.object({ request: v.string() }), }), inputMismatch: build.query<Post, { id: number }>({ query: ({ id }) => `/post/${id}`, // @ts-expect-error can't expect different input argSchema: v.object({ id: v.pipe(v.string(), v.transform(Number), v.number()), }), // @ts-expect-error can't expect different input responseSchema: v.object({ ...postSchema.entries, id: v.pipe(v.string(), v.transform(Number)), }) satisfies v.GenericSchema<any, Post>, // @ts-expect-error can't expect different input errorResponseSchema: v.object({ ...errorResponseSchema.entries, status: v.pipe(v.string(), v.transform(Number)), }) satisfies v.GenericSchema<any, FetchBaseQueryError>, // @ts-expect-error can't expect different input metaSchema: v.object({ ...metaSchema.entries, request: v.pipe( v.string(), v.transform((url) => new Request(url)), ), }) satisfies v.GenericSchema<any, FetchBaseQueryMeta>, }), outputMismatch: build.query<Post, { id: number }>({ query: ({ id }) => `/post/${id}`, // @ts-expect-error can't provide different output argSchema: v.object({ id: v.pipe(v.number(), v.transform(String)), }), // @ts-expect-error can't provide different output responseSchema: v.object({ ...postSchema.entries, id: v.pipe(v.number(), v.transform(String)), }) satisfies v.GenericSchema<Post, any>, // @ts-expect-error can't provide different output errorResponseSchema: v.object({ ...errorResponseSchema.entries, status: v.pipe(v.number(), v.transform(String)), }) satisfies v.GenericSchema<FetchBaseQueryError, any>, // @ts-expect-error can't provide different output metaSchema: v.object({ ...metaSchema.entries, request: v.pipe( v.instance(Request), v.transform((r) => r.url), ), }) satisfies v.GenericSchema<FetchBaseQueryMeta, any>, }), }), }) }) test('schemas as a source of inference', () => { const postAdapter = createEntityAdapter<Post>() const api = createApi({ baseQuery: fetchBaseQuery({ baseUrl: 'https://example.com' }), endpoints: (build) => ({ query: build.query({ query: ({ id }: { id: number }) => `/post/${id}`, responseSchema: postSchema, }), query2: build.query({ query: (arg) => { expectTypeOf(arg).toEqualTypeOf<{ id: number }>() return `/post/${arg.id}` }, argSchema, responseSchema: postSchema, }), query3: build.query({ query: (_arg: void) => `/posts`, rawResponseSchema: v.array(postSchema), transformResponse: (posts) => { expectTypeOf(posts).toEqualTypeOf<Post[]>() return postAdapter.getInitialState(undefined, posts) }, }), }), }) expectTypeOf(api.endpoints.query.Types.QueryArg).toEqualTypeOf<{ id: number }>() expectTypeOf(api.endpoints.query.Types.ResultType).toEqualTypeOf<Post>() expectTypeOf(api.endpoints.query2.Types.QueryArg).toEqualTypeOf<{ id: number }>() expectTypeOf( api.endpoints.query2.Types.ResultType, ).toEqualTypeOf<Post>() expectTypeOf(api.endpoints.query3.Types.QueryArg).toEqualTypeOf<void>() expectTypeOf(api.endpoints.query3.Types.ResultType).toEqualTypeOf< EntityState<Post, Post['id']> >() }) }) }) })