Skip to content

Commit c855b8b

Browse files
authored
Merge pull request #4656 from aryaemami59/add-TypedQueryStateSelector
Add the `TypedQueryStateSelector` helper type
2 parents b7b3df6 + 37b635a commit c855b8b

File tree

6 files changed

+256
-3
lines changed

6 files changed

+256
-3
lines changed

packages/toolkit/src/query/baseQueryTypes.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,24 +63,39 @@ export type BaseQueryEnhancer<
6363
NonNullable<BaseQueryMeta<BaseQuery>>
6464
>
6565

66+
/**
67+
* @public
68+
*/
6669
export type BaseQueryResult<BaseQuery extends BaseQueryFn> =
6770
UnwrapPromise<ReturnType<BaseQuery>> extends infer Unwrapped
6871
? Unwrapped extends { data: any }
6972
? Unwrapped['data']
7073
: never
7174
: never
7275

76+
/**
77+
* @public
78+
*/
7379
export type BaseQueryMeta<BaseQuery extends BaseQueryFn> = UnwrapPromise<
7480
ReturnType<BaseQuery>
7581
>['meta']
7682

83+
/**
84+
* @public
85+
*/
7786
export type BaseQueryError<BaseQuery extends BaseQueryFn> = Exclude<
7887
UnwrapPromise<ReturnType<BaseQuery>>,
7988
{ error?: undefined }
8089
>['error']
8190

91+
/**
92+
* @public
93+
*/
8294
export type BaseQueryArg<T extends (arg: any, ...args: any[]) => any> =
8395
T extends (arg: infer A, ...args: any[]) => any ? A : any
8496

97+
/**
98+
* @public
99+
*/
85100
export type BaseQueryExtraOptions<BaseQuery extends BaseQueryFn> =
86101
Parameters<BaseQuery>[2]

packages/toolkit/src/query/endpointDefinitions.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -231,6 +231,10 @@ export type FullTagDescription<TagType> = {
231231
id?: number | string
232232
}
233233
export type TagDescription<TagType> = TagType | FullTagDescription<TagType>
234+
235+
/**
236+
* @public
237+
*/
234238
export type ResultDescription<
235239
TagTypes extends string,
236240
ResultType,

packages/toolkit/src/query/index.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,14 @@ export type { Api, ApiContext, Module } from './apiTypes'
1515

1616
export type {
1717
BaseQueryApi,
18+
BaseQueryArg,
1819
BaseQueryEnhancer,
20+
BaseQueryError,
21+
BaseQueryExtraOptions,
1922
BaseQueryFn,
20-
QueryReturnValue
23+
BaseQueryMeta,
24+
BaseQueryResult,
25+
QueryReturnValue,
2126
} from './baseQueryTypes'
2227
export type {
2328
BaseEndpointDefinition,
@@ -34,6 +39,7 @@ export type {
3439
DefinitionType,
3540
DefinitionsFromApi,
3641
OverrideResultType,
42+
ResultDescription,
3743
TagTypesFromApi,
3844
UpdateDefinitions,
3945
} from './endpointDefinitions'

packages/toolkit/src/query/react/buildHooks.ts

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -327,11 +327,127 @@ export type TypedUseLazyQuerySubscription<
327327
QueryDefinition<QueryArg, BaseQuery, string, ResultType, string>
328328
>
329329

330+
/**
331+
* @internal
332+
*/
330333
export type QueryStateSelector<
331334
R extends Record<string, any>,
332335
D extends QueryDefinition<any, any, any, any>,
333336
> = (state: UseQueryStateDefaultResult<D>) => R
334337

338+
/**
339+
* Provides a way to define a strongly-typed version of
340+
* {@linkcode QueryStateSelector} for use with a specific query.
341+
* This is useful for scenarios where you want to create a "pre-typed"
342+
* {@linkcode UseQueryStateOptions.selectFromResult | selectFromResult}
343+
* function.
344+
*
345+
* @example
346+
* <caption>#### __Create a strongly-typed `selectFromResult` selector function__</caption>
347+
*
348+
* ```tsx
349+
* import type { TypedQueryStateSelector } from '@reduxjs/toolkit/query/react'
350+
* import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'
351+
*
352+
* type Post = {
353+
* id: number
354+
* title: string
355+
* }
356+
*
357+
* type PostsApiResponse = {
358+
* posts: Post[]
359+
* total: number
360+
* skip: number
361+
* limit: number
362+
* }
363+
*
364+
* type QueryArgument = number | undefined
365+
*
366+
* type BaseQueryFunction = ReturnType<typeof fetchBaseQuery>
367+
*
368+
* type SelectedResult = Pick<PostsApiResponse, 'posts'>
369+
*
370+
* const postsApiSlice = createApi({
371+
* baseQuery: fetchBaseQuery({ baseUrl: 'https://dummyjson.com/posts' }),
372+
* reducerPath: 'postsApi',
373+
* tagTypes: ['Posts'],
374+
* endpoints: (build) => ({
375+
* getPosts: build.query<PostsApiResponse, QueryArgument>({
376+
* query: (limit = 5) => `?limit=${limit}&select=title`,
377+
* }),
378+
* }),
379+
* })
380+
*
381+
* const { useGetPostsQuery } = postsApiSlice
382+
*
383+
* function PostById({ id }: { id: number }) {
384+
* const { post } = useGetPostsQuery(undefined, {
385+
* selectFromResult: (state) => ({
386+
* post: state.data?.posts.find((post) => post.id === id),
387+
* }),
388+
* })
389+
*
390+
* return <li>{post?.title}</li>
391+
* }
392+
*
393+
* const EMPTY_ARRAY: Post[] = []
394+
*
395+
* const typedSelectFromResult: TypedQueryStateSelector<
396+
* PostsApiResponse,
397+
* QueryArgument,
398+
* BaseQueryFunction,
399+
* SelectedResult
400+
* > = (state) => ({ posts: state.data?.posts ?? EMPTY_ARRAY })
401+
*
402+
* function PostsList() {
403+
* const { posts } = useGetPostsQuery(undefined, {
404+
* selectFromResult: typedSelectFromResult,
405+
* })
406+
*
407+
* return (
408+
* <div>
409+
* <ul>
410+
* {posts.map((post) => (
411+
* <PostById key={post.id} id={post.id} />
412+
* ))}
413+
* </ul>
414+
* </div>
415+
* )
416+
* }
417+
* ```
418+
*
419+
* @template ResultType - The type of the result `data` returned by the query.
420+
* @template QueryArgumentType - The type of the argument passed into the query.
421+
* @template BaseQueryFunctionType - The type of the base query function being used.
422+
* @template SelectedResultType - The type of the selected result returned by the __`selectFromResult`__ function.
423+
*
424+
* @since 2.7.9
425+
* @public
426+
*/
427+
export type TypedQueryStateSelector<
428+
ResultType,
429+
QueryArgumentType,
430+
BaseQueryFunctionType extends BaseQueryFn,
431+
SelectedResultType extends Record<string, any> = UseQueryStateDefaultResult<
432+
QueryDefinition<
433+
QueryArgumentType,
434+
BaseQueryFunctionType,
435+
string,
436+
ResultType,
437+
string
438+
>
439+
>,
440+
> = QueryStateSelector<
441+
SelectedResultType,
442+
QueryDefinition<
443+
QueryArgumentType,
444+
BaseQueryFunctionType,
445+
string,
446+
ResultType,
447+
string
448+
>
449+
>
450+
335451
/**
336452
* A React hook that reads the request status and cached data from the Redux store. The component will re-render as the loading status changes and the data becomes available.
337453
*

packages/toolkit/src/query/react/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ export type {
2222
TypedUseLazyQuery,
2323
TypedUseMutation,
2424
TypedMutationTrigger,
25+
TypedQueryStateSelector,
2526
TypedUseQueryState,
2627
TypedUseQuery,
2728
TypedUseQuerySubscription,

packages/toolkit/src/query/tests/buildHooks.test-d.tsx

Lines changed: 113 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,15 @@
1-
import type { UseMutation, UseQuery } from '@internal/query/react/buildHooks'
1+
import type {
2+
QueryStateSelector,
3+
UseMutation,
4+
UseQuery,
5+
} from '@internal/query/react/buildHooks'
26
import { ANY } from '@internal/tests/utils/helpers'
37
import type { SerializedError } from '@reduxjs/toolkit'
4-
import type { SubscriptionOptions } from '@reduxjs/toolkit/query/react'
8+
import type {
9+
QueryDefinition,
10+
SubscriptionOptions,
11+
TypedQueryStateSelector,
12+
} from '@reduxjs/toolkit/query/react'
513
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'
614
import { useState } from 'react'
715

@@ -260,4 +268,107 @@ describe('type tests', () => {
260268
api.endpoints.updateUser.useMutation,
261269
)
262270
})
271+
272+
test('TypedQueryStateSelector creates a pre-typed version of QueryStateSelector', () => {
273+
type Post = {
274+
id: number
275+
title: string
276+
}
277+
278+
type PostsApiResponse = {
279+
posts: Post[]
280+
total: number
281+
skip: number
282+
limit: number
283+
}
284+
285+
type QueryArgument = number | undefined
286+
287+
type BaseQueryFunction = ReturnType<typeof fetchBaseQuery>
288+
289+
type SelectedResult = Pick<PostsApiResponse, 'posts'>
290+
291+
const postsApiSlice = createApi({
292+
baseQuery: fetchBaseQuery({ baseUrl: 'https://dummyjson.com/posts' }),
293+
reducerPath: 'postsApi',
294+
tagTypes: ['Posts'],
295+
endpoints: (build) => ({
296+
getPosts: build.query<PostsApiResponse, QueryArgument>({
297+
query: (limit = 5) => `?limit=${limit}&select=title`,
298+
}),
299+
}),
300+
})
301+
302+
const { useGetPostsQuery } = postsApiSlice
303+
304+
function PostById({ id }: { id: number }) {
305+
const { post } = useGetPostsQuery(undefined, {
306+
selectFromResult: (state) => ({
307+
post: state.data?.posts.find((post) => post.id === id),
308+
}),
309+
})
310+
311+
expectTypeOf(post).toEqualTypeOf<Post | undefined>()
312+
313+
return <li>{post?.title}</li>
314+
}
315+
316+
const EMPTY_ARRAY: Post[] = []
317+
318+
const typedSelectFromResult: TypedQueryStateSelector<
319+
PostsApiResponse,
320+
QueryArgument,
321+
BaseQueryFunction,
322+
SelectedResult
323+
> = (state) => ({ posts: state.data?.posts ?? EMPTY_ARRAY })
324+
325+
expectTypeOf<
326+
TypedQueryStateSelector<
327+
PostsApiResponse,
328+
QueryArgument,
329+
BaseQueryFunction,
330+
SelectedResult
331+
>
332+
>().toEqualTypeOf<
333+
QueryStateSelector<
334+
SelectedResult,
335+
QueryDefinition<
336+
QueryArgument,
337+
BaseQueryFunction,
338+
string,
339+
PostsApiResponse
340+
>
341+
>
342+
>()
343+
344+
expectTypeOf(typedSelectFromResult).toEqualTypeOf<
345+
QueryStateSelector<
346+
SelectedResult,
347+
QueryDefinition<
348+
QueryArgument,
349+
BaseQueryFunction,
350+
string,
351+
PostsApiResponse
352+
>
353+
>
354+
>()
355+
356+
function PostsList() {
357+
const { posts } = useGetPostsQuery(undefined, {
358+
selectFromResult: typedSelectFromResult,
359+
})
360+
361+
expectTypeOf(posts).toEqualTypeOf<Post[]>()
362+
363+
return (
364+
<div>
365+
<ul>
366+
{posts.map((post) => (
367+
<PostById key={post.id} id={post.id} />
368+
))}
369+
</ul>
370+
</div>
371+
)
372+
}
373+
})
263374
})

0 commit comments

Comments
 (0)