From d919262e20876c53c6b8c8eb0ea16597968ed0b5 Mon Sep 17 00:00:00 2001 From: minsuKang Date: Wed, 16 Jul 2025 15:33:59 +0900 Subject: [PATCH 1/2] fix: :bug: openapi-react-query type definition fix --- packages/openapi-react-query/src/index.ts | 149 +++++++++++++--------- 1 file changed, 92 insertions(+), 57 deletions(-) diff --git a/packages/openapi-react-query/src/index.ts b/packages/openapi-react-query/src/index.ts index 0ca534c34..12744f4f9 100644 --- a/packages/openapi-react-query/src/index.ts +++ b/packages/openapi-react-query/src/index.ts @@ -15,15 +15,20 @@ import { useQuery, useSuspenseQuery, useInfiniteQuery, -} from "@tanstack/react-query"; +} from '@tanstack/react-query'; import type { ClientMethod, FetchResponse, MaybeOptionalInit, Client as FetchClient, DefaultParamsOption, -} from "openapi-fetch"; -import type { HttpMethod, MediaType, PathsWithMethod, RequiredKeysOf } from "openapi-typescript-helpers"; +} from 'openapi-fetch'; +import type { + HttpMethod, + MediaType, + PathsWithMethod, + RequiredKeysOf, +} from 'openapi-typescript-helpers'; // Helper type to dynamically infer the type from the `select` property type InferSelectReturnType = TSelect extends (data: TData) => infer R ? R : TData; @@ -37,19 +42,22 @@ export type QueryKey< Init = MaybeOptionalInit, > = Init extends undefined ? readonly [Method, Path] : readonly [Method, Path, Init]; -export type QueryOptionsFunction>, Media extends MediaType> = < +export type QueryOptionsFunction< + Paths extends Record>, + Media extends MediaType, +> = < Method extends HttpMethod, Path extends PathsWithMethod, Init extends MaybeOptionalInit, Response extends Required>, // note: Required is used to avoid repeating NonNullable in UseQuery types Options extends Omit< UseQueryOptions< - Response["data"], - Response["error"], - InferSelectReturnType, + Response['data'], + Response['error'], + InferSelectReturnType, QueryKey >, - "queryKey" | "queryFn" + 'queryKey' | 'queryFn' >, >( method: Method, @@ -60,38 +68,41 @@ export type QueryOptionsFunction NoInfer< Omit< UseQueryOptions< - Response["data"], - Response["error"], - InferSelectReturnType, + Response['data'], + Response['error'], + InferSelectReturnType, QueryKey >, - "queryFn" + 'queryFn' > & { queryFn: Exclude< UseQueryOptions< - Response["data"], - Response["error"], - InferSelectReturnType, + Response['data'], + Response['error'], + InferSelectReturnType, QueryKey - >["queryFn"], + >['queryFn'], SkipToken | undefined >; } >; -export type UseQueryMethod>, Media extends MediaType> = < +export type UseQueryMethod< + Paths extends Record>, + Media extends MediaType, +> = < Method extends HttpMethod, Path extends PathsWithMethod, Init extends MaybeOptionalInit, Response extends Required>, // note: Required is used to avoid repeating NonNullable in UseQuery types Options extends Omit< UseQueryOptions< - Response["data"], - Response["error"], - InferSelectReturnType, + Response['data'], + Response['error'], + InferSelectReturnType, QueryKey >, - "queryKey" | "queryFn" + 'queryKey' | 'queryFn' >, >( method: Method, @@ -99,50 +110,57 @@ export type UseQueryMethod>, ...[init, options, queryClient]: RequiredKeysOf extends never ? [InitWithUnknowns?, Options?, QueryClient?] : [InitWithUnknowns, Options?, QueryClient?] -) => UseQueryResult, Response["error"]>; +) => UseQueryResult, Response['error']>; -export type UseInfiniteQueryMethod>, Media extends MediaType> = < +export type UseInfiniteQueryMethod< + Paths extends Record>, + Media extends MediaType, +> = < Method extends HttpMethod, Path extends PathsWithMethod, Init extends MaybeOptionalInit, Response extends Required>, + Select = undefined, Options extends Omit< UseInfiniteQueryOptions< - Response["data"], - Response["error"], - InferSelectReturnType, Options["select"]>, - Response["data"], + Response['data'], + Response['error'], + InferSelectReturnType, Select>, QueryKey, unknown >, - "queryKey" | "queryFn" + 'queryKey' | 'queryFn' > & { pageParamName?: string; - }, + select?: Select; + } = any, >( method: Method, url: Path, init: InitWithUnknowns, options: Options, - queryClient?: QueryClient, + queryClient?: QueryClient ) => UseInfiniteQueryResult< - InferSelectReturnType, Options["select"]>, - Response["error"] + InferSelectReturnType, Select>, + Response['error'] >; -export type UseSuspenseQueryMethod>, Media extends MediaType> = < +export type UseSuspenseQueryMethod< + Paths extends Record>, + Media extends MediaType, +> = < Method extends HttpMethod, Path extends PathsWithMethod, Init extends MaybeOptionalInit, Response extends Required>, // note: Required is used to avoid repeating NonNullable in UseQuery types Options extends Omit< UseSuspenseQueryOptions< - Response["data"], - Response["error"], - InferSelectReturnType, + Response['data'], + Response['error'], + InferSelectReturnType, QueryKey >, - "queryKey" | "queryFn" + 'queryKey' | 'queryFn' >, >( method: Method, @@ -150,20 +168,29 @@ export type UseSuspenseQueryMethod extends never ? [InitWithUnknowns?, Options?, QueryClient?] : [InitWithUnknowns, Options?, QueryClient?] -) => UseSuspenseQueryResult, Response["error"]>; +) => UseSuspenseQueryResult< + InferSelectReturnType, + Response['error'] +>; -export type UseMutationMethod>, Media extends MediaType> = < +export type UseMutationMethod< + Paths extends Record>, + Media extends MediaType, +> = < Method extends HttpMethod, Path extends PathsWithMethod, Init extends MaybeOptionalInit, Response extends Required>, // note: Required is used to avoid repeating NonNullable in UseQuery types - Options extends Omit, "mutationKey" | "mutationFn">, + Options extends Omit< + UseMutationOptions, + 'mutationKey' | 'mutationFn' + >, >( method: Method, url: Path, options?: Options, - queryClient?: QueryClient, -) => UseMutationResult; + queryClient?: QueryClient +) => UseMutationResult; export interface OpenapiQueryClient { queryOptions: QueryOptionsFunction; @@ -180,13 +207,17 @@ export type MethodResponse< ? PathsWithMethod : never, Options = object, -> = CreatedClient extends OpenapiQueryClient - ? NonNullable["data"]> - : never; +> = + CreatedClient extends OpenapiQueryClient< + infer Paths extends { [key: string]: any }, + infer Media extends MediaType + > + ? NonNullable['data']> + : never; // TODO: Add the ability to bring queryClient as argument export default function createClient( - client: FetchClient, + client: FetchClient ): OpenapiQueryClient { const queryFn = async >({ queryKey: [method, path, init], @@ -198,7 +229,7 @@ export default function createClient = (method, path, ...[init, options]) => ({ - queryKey: (init === undefined ? ([method, path] as const) : ([method, path, init] as const)) as QueryKey< - Paths, - typeof method, - typeof path - >, + queryKey: (init === undefined + ? ([method, path] as const) + : ([method, path, init] as const)) as QueryKey, queryFn, ...options, }); @@ -218,11 +247,17 @@ export default function createClient - useQuery(queryOptions(method, path, init as InitWithUnknowns, options), queryClient), + useQuery( + queryOptions(method, path, init as InitWithUnknowns, options), + queryClient + ), useSuspenseQuery: (method, path, ...[init, options, queryClient]) => - useSuspenseQuery(queryOptions(method, path, init as InitWithUnknowns, options), queryClient), + useSuspenseQuery( + queryOptions(method, path, init as InitWithUnknowns, options), + queryClient + ), useInfiniteQuery: (method, path, init, options, queryClient) => { - const { pageParamName = "cursor", ...restOptions } = options; + const { pageParamName = 'cursor', ...restOptions } = options; const { queryKey } = queryOptions(method, path, init); return useInfiniteQuery( { @@ -250,7 +285,7 @@ export default function createClient @@ -269,7 +304,7 @@ export default function createClient Date: Thu, 17 Jul 2025 21:23:11 +0900 Subject: [PATCH 2/2] fix: :bug: useInfiniteQuery type safe fix --- packages/openapi-react-query/src/index.ts | 163 +++++++++------------- 1 file changed, 68 insertions(+), 95 deletions(-) diff --git a/packages/openapi-react-query/src/index.ts b/packages/openapi-react-query/src/index.ts index 12744f4f9..beccc929d 100644 --- a/packages/openapi-react-query/src/index.ts +++ b/packages/openapi-react-query/src/index.ts @@ -15,20 +15,15 @@ import { useQuery, useSuspenseQuery, useInfiniteQuery, -} from '@tanstack/react-query'; +} from "@tanstack/react-query"; import type { ClientMethod, FetchResponse, MaybeOptionalInit, Client as FetchClient, DefaultParamsOption, -} from 'openapi-fetch'; -import type { - HttpMethod, - MediaType, - PathsWithMethod, - RequiredKeysOf, -} from 'openapi-typescript-helpers'; +} from "openapi-fetch"; +import type { HttpMethod, MediaType, PathsWithMethod, RequiredKeysOf } from "openapi-typescript-helpers"; // Helper type to dynamically infer the type from the `select` property type InferSelectReturnType = TSelect extends (data: TData) => infer R ? R : TData; @@ -42,22 +37,19 @@ export type QueryKey< Init = MaybeOptionalInit, > = Init extends undefined ? readonly [Method, Path] : readonly [Method, Path, Init]; -export type QueryOptionsFunction< - Paths extends Record>, - Media extends MediaType, -> = < +export type QueryOptionsFunction>, Media extends MediaType> = < Method extends HttpMethod, Path extends PathsWithMethod, Init extends MaybeOptionalInit, Response extends Required>, // note: Required is used to avoid repeating NonNullable in UseQuery types Options extends Omit< UseQueryOptions< - Response['data'], - Response['error'], - InferSelectReturnType, + Response["data"], + Response["error"], + InferSelectReturnType, QueryKey >, - 'queryKey' | 'queryFn' + "queryKey" | "queryFn" >, >( method: Method, @@ -68,41 +60,38 @@ export type QueryOptionsFunction< ) => NoInfer< Omit< UseQueryOptions< - Response['data'], - Response['error'], - InferSelectReturnType, + Response["data"], + Response["error"], + InferSelectReturnType, QueryKey >, - 'queryFn' + "queryFn" > & { queryFn: Exclude< UseQueryOptions< - Response['data'], - Response['error'], - InferSelectReturnType, + Response["data"], + Response["error"], + InferSelectReturnType, QueryKey - >['queryFn'], + >["queryFn"], SkipToken | undefined >; } >; -export type UseQueryMethod< - Paths extends Record>, - Media extends MediaType, -> = < +export type UseQueryMethod>, Media extends MediaType> = < Method extends HttpMethod, Path extends PathsWithMethod, Init extends MaybeOptionalInit, Response extends Required>, // note: Required is used to avoid repeating NonNullable in UseQuery types Options extends Omit< UseQueryOptions< - Response['data'], - Response['error'], - InferSelectReturnType, + Response["data"], + Response["error"], + InferSelectReturnType, QueryKey >, - 'queryKey' | 'queryFn' + "queryKey" | "queryFn" >, >( method: Method, @@ -110,57 +99,53 @@ export type UseQueryMethod< ...[init, options, queryClient]: RequiredKeysOf extends never ? [InitWithUnknowns?, Options?, QueryClient?] : [InitWithUnknowns, Options?, QueryClient?] -) => UseQueryResult, Response['error']>; +) => UseQueryResult, Response["error"]>; -export type UseInfiniteQueryMethod< - Paths extends Record>, - Media extends MediaType, -> = < +// Helper type to infer TPageParam type +type InferPageParamType = T extends { initialPageParam: infer P } ? P : unknown; + +export type UseInfiniteQueryMethod>, Media extends MediaType> = < Method extends HttpMethod, Path extends PathsWithMethod, Init extends MaybeOptionalInit, Response extends Required>, - Select = undefined, Options extends Omit< UseInfiniteQueryOptions< - Response['data'], - Response['error'], - InferSelectReturnType, Select>, + Response["data"], + Response["error"], + InferSelectReturnType, Options["select"]>, QueryKey, - unknown + InferPageParamType >, - 'queryKey' | 'queryFn' + "queryKey" | "queryFn" > & { pageParamName?: string; - select?: Select; - } = any, + initialPageParam: InferPageParamType; + }, >( method: Method, url: Path, init: InitWithUnknowns, options: Options, - queryClient?: QueryClient + queryClient?: QueryClient, ) => UseInfiniteQueryResult< - InferSelectReturnType, Select>, - Response['error'] + InferSelectReturnType, Options["select"]>, + Response["error"] >; -export type UseSuspenseQueryMethod< - Paths extends Record>, - Media extends MediaType, -> = < +export type UseSuspenseQueryMethod>, Media extends MediaType> = < Method extends HttpMethod, Path extends PathsWithMethod, Init extends MaybeOptionalInit, Response extends Required>, // note: Required is used to avoid repeating NonNullable in UseQuery types Options extends Omit< UseSuspenseQueryOptions< - Response['data'], - Response['error'], - InferSelectReturnType, + Response["data"], + Response["error"], + InferSelectReturnType, QueryKey >, - 'queryKey' | 'queryFn' + "queryKey" | "queryFn" >, >( method: Method, @@ -168,29 +153,20 @@ export type UseSuspenseQueryMethod< ...[init, options, queryClient]: RequiredKeysOf extends never ? [InitWithUnknowns?, Options?, QueryClient?] : [InitWithUnknowns, Options?, QueryClient?] -) => UseSuspenseQueryResult< - InferSelectReturnType, - Response['error'] ->; +) => UseSuspenseQueryResult, Response["error"]>; -export type UseMutationMethod< - Paths extends Record>, - Media extends MediaType, -> = < +export type UseMutationMethod>, Media extends MediaType> = < Method extends HttpMethod, Path extends PathsWithMethod, Init extends MaybeOptionalInit, Response extends Required>, // note: Required is used to avoid repeating NonNullable in UseQuery types - Options extends Omit< - UseMutationOptions, - 'mutationKey' | 'mutationFn' - >, + Options extends Omit, "mutationKey" | "mutationFn">, >( method: Method, url: Path, options?: Options, - queryClient?: QueryClient -) => UseMutationResult; + queryClient?: QueryClient, +) => UseMutationResult; export interface OpenapiQueryClient { queryOptions: QueryOptionsFunction; @@ -207,17 +183,13 @@ export type MethodResponse< ? PathsWithMethod : never, Options = object, -> = - CreatedClient extends OpenapiQueryClient< - infer Paths extends { [key: string]: any }, - infer Media extends MediaType - > - ? NonNullable['data']> - : never; +> = CreatedClient extends OpenapiQueryClient + ? NonNullable["data"]> + : never; // TODO: Add the ability to bring queryClient as argument export default function createClient( - client: FetchClient + client: FetchClient, ): OpenapiQueryClient { const queryFn = async >({ queryKey: [method, path, init], @@ -225,11 +197,14 @@ export default function createClient>) => { const mth = method.toUpperCase() as Uppercase; const fn = client[mth] as ClientMethod; - const { data, error, response } = await fn(path, { signal, ...(init as any) }); // TODO: find a way to avoid as any + const { data, error, response } = await fn(path, { + signal, + ...(init as any), + }); // TODO: find a way to avoid as any if (error) { throw error; } - if (response.status === 204 || response.headers.get('Content-Length') === '0') { + if (response.status === 204 || response.headers.get("Content-Length") === "0") { return data ?? null; } @@ -237,9 +212,11 @@ export default function createClient = (method, path, ...[init, options]) => ({ - queryKey: (init === undefined - ? ([method, path] as const) - : ([method, path, init] as const)) as QueryKey, + queryKey: (init === undefined ? ([method, path] as const) : ([method, path, init] as const)) as QueryKey< + Paths, + typeof method, + typeof path + >, queryFn, ...options, }); @@ -247,22 +224,18 @@ export default function createClient - useQuery( - queryOptions(method, path, init as InitWithUnknowns, options), - queryClient - ), + useQuery(queryOptions(method, path, init as InitWithUnknowns, options), queryClient), useSuspenseQuery: (method, path, ...[init, options, queryClient]) => - useSuspenseQuery( - queryOptions(method, path, init as InitWithUnknowns, options), - queryClient - ), + useSuspenseQuery(queryOptions(method, path, init as InitWithUnknowns, options), queryClient), useInfiniteQuery: (method, path, init, options, queryClient) => { - const { pageParamName = 'cursor', ...restOptions } = options; + const { pageParamName = "cursor", initialPageParam, ...restOptions } = options; const { queryKey } = queryOptions(method, path, init); + return useInfiniteQuery( { queryKey, - queryFn: async ({ queryKey: [method, path, init], pageParam = 0, signal }) => { + initialPageParam, + queryFn: async ({ queryKey: [method, path, init], pageParam, signal }) => { const mth = method.toUpperCase() as Uppercase; const fn = client[mth] as ClientMethod; const mergedInit = { @@ -285,7 +258,7 @@ export default function createClient @@ -304,7 +277,7 @@ export default function createClient