Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions packages/toolkit/src/query/baseQueryTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,24 +63,39 @@ export type BaseQueryEnhancer<
NonNullable<BaseQueryMeta<BaseQuery>>
>

/**
* @public
*/
export type BaseQueryResult<BaseQuery extends BaseQueryFn> =
UnwrapPromise<ReturnType<BaseQuery>> extends infer Unwrapped
? Unwrapped extends { data: any }
? Unwrapped['data']
: never
: never

/**
* @public
*/
export type BaseQueryMeta<BaseQuery extends BaseQueryFn> = UnwrapPromise<
ReturnType<BaseQuery>
>['meta']

/**
* @public
*/
export type BaseQueryError<BaseQuery extends BaseQueryFn> = Exclude<
UnwrapPromise<ReturnType<BaseQuery>>,
{ error?: undefined }
>['error']

/**
* @public
*/
export type BaseQueryArg<T extends (arg: any, ...args: any[]) => any> =
T extends (arg: infer A, ...args: any[]) => any ? A : any

/**
* @public
*/
export type BaseQueryExtraOptions<BaseQuery extends BaseQueryFn> =
Parameters<BaseQuery>[2]
4 changes: 4 additions & 0 deletions packages/toolkit/src/query/endpointDefinitions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,10 @@ export type FullTagDescription<TagType> = {
id?: number | string
}
export type TagDescription<TagType> = TagType | FullTagDescription<TagType>

/**
* @public
*/
export type ResultDescription<
TagTypes extends string,
ResultType,
Expand Down
8 changes: 7 additions & 1 deletion packages/toolkit/src/query/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,14 @@ export type { Api, ApiContext, Module } from './apiTypes'

export type {
BaseQueryApi,
BaseQueryArg,
BaseQueryEnhancer,
BaseQueryError,
BaseQueryExtraOptions,
BaseQueryFn,
QueryReturnValue
BaseQueryMeta,
BaseQueryResult,
QueryReturnValue,
} from './baseQueryTypes'
export type {
BaseEndpointDefinition,
Expand All @@ -34,6 +39,7 @@ export type {
DefinitionType,
DefinitionsFromApi,
OverrideResultType,
ResultDescription,
TagTypesFromApi,
UpdateDefinitions,
} from './endpointDefinitions'
Expand Down
116 changes: 116 additions & 0 deletions packages/toolkit/src/query/react/buildHooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -327,11 +327,127 @@ export type TypedUseLazyQuerySubscription<
QueryDefinition<QueryArg, BaseQuery, string, ResultType, string>
>

/**
* @internal
*/
export type QueryStateSelector<
R extends Record<string, any>,
D extends QueryDefinition<any, any, any, any>,
> = (state: UseQueryStateDefaultResult<D>) => R

/**
* Provides a way to define a strongly-typed version of
* {@linkcode QueryStateSelector} for use with a specific query.
* This is useful for scenarios where you want to create a "pre-typed"
* {@linkcode UseQueryStateOptions.selectFromResult | selectFromResult}
* function.
*
* @example
* <caption>#### __Create a strongly-typed `selectFromResult` selector function__</caption>
*
* ```tsx
* import type { TypedQueryStateSelector } from '@reduxjs/toolkit/query/react'
* import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'
*
* type Post = {
* id: number
* title: string
* }
*
* type PostsApiResponse = {
* posts: Post[]
* total: number
* skip: number
* limit: number
* }
*
* type QueryArgument = number | undefined
*
* type BaseQueryFunction = ReturnType<typeof fetchBaseQuery>
*
* type SelectedResult = Pick<PostsApiResponse, 'posts'>
*
* const postsApiSlice = createApi({
* baseQuery: fetchBaseQuery({ baseUrl: 'https://dummyjson.com/posts' }),
* reducerPath: 'postsApi',
* tagTypes: ['Posts'],
* endpoints: (build) => ({
* getPosts: build.query<PostsApiResponse, QueryArgument>({
* 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),
* }),
* })
*
* return <li>{post?.title}</li>
* }
*
* const EMPTY_ARRAY: Post[] = []
*
* const typedSelectFromResult: TypedQueryStateSelector<
* PostsApiResponse,
* QueryArgument,
* BaseQueryFunction,
* SelectedResult
* > = (state) => ({ posts: state.data?.posts ?? EMPTY_ARRAY })
*
* function PostsList() {
* const { posts } = useGetPostsQuery(undefined, {
* selectFromResult: typedSelectFromResult,
* })
*
* return (
* <div>
* <ul>
* {posts.map((post) => (
* <PostById key={post.id} id={post.id} />
* ))}
* </ul>
* </div>
* )
* }
* ```
*
* @template ResultType - The type of the result `data` returned by the query.
* @template QueryArgumentType - The type of the argument passed into the query.
* @template BaseQueryFunctionType - The type of the base query function being used.
* @template SelectedResultType - The type of the selected result returned by the __`selectFromResult`__ function.
*
* @since 2.7.9
* @public
*/
export type TypedQueryStateSelector<
ResultType,
QueryArgumentType,
BaseQueryFunctionType extends BaseQueryFn,
SelectedResultType extends Record<string, any> = UseQueryStateDefaultResult<
QueryDefinition<
QueryArgumentType,
BaseQueryFunctionType,
string,
ResultType,
string
>
>,
> = QueryStateSelector<
SelectedResultType,
QueryDefinition<
QueryArgumentType,
BaseQueryFunctionType,
string,
ResultType,
string
>
>

/**
* 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.
*
Expand Down
1 change: 1 addition & 0 deletions packages/toolkit/src/query/react/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export type {
TypedUseLazyQuery,
TypedUseMutation,
TypedMutationTrigger,
TypedQueryStateSelector,
TypedUseQueryState,
TypedUseQuery,
TypedUseQuerySubscription,
Expand Down
115 changes: 113 additions & 2 deletions packages/toolkit/src/query/tests/buildHooks.test-d.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,15 @@
import type { UseMutation, UseQuery } from '@internal/query/react/buildHooks'
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 { SubscriptionOptions } from '@reduxjs/toolkit/query/react'
import type {
QueryDefinition,
SubscriptionOptions,
TypedQueryStateSelector,
} from '@reduxjs/toolkit/query/react'
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'
import { useState } from 'react'

Expand Down Expand Up @@ -260,4 +268,107 @@ describe('type tests', () => {
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<typeof fetchBaseQuery>

type SelectedResult = Pick<PostsApiResponse, 'posts'>

const postsApiSlice = createApi({
baseQuery: fetchBaseQuery({ baseUrl: 'https://dummyjson.com/posts' }),
reducerPath: 'postsApi',
tagTypes: ['Posts'],
endpoints: (build) => ({
getPosts: build.query<PostsApiResponse, QueryArgument>({
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<Post | undefined>()

return <li>{post?.title}</li>
}

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<Post[]>()

return (
<div>
<ul>
{posts.map((post) => (
<PostById key={post.id} id={post.id} />
))}
</ul>
</div>
)
}
})
})