Skip to content
Draft
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
10 changes: 9 additions & 1 deletion examples/openapi-ts-tanstack-react-query/openapi-ts.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@ export default defineConfig({
lint: 'eslint',
path: './src/client',
},
parser: {
pagination: {
keywords: ['tags'],
},
},
plugins: [
'@hey-api/client-fetch',
'@hey-api/schemas',
Expand All @@ -19,6 +24,9 @@ export default defineConfig({
enums: 'javascript',
name: '@hey-api/typescript',
},
'@tanstack/react-query',
{
infiniteQueryOptions: true,
name: '@tanstack/react-query',
},
],
});
30 changes: 29 additions & 1 deletion examples/openapi-ts-tanstack-react-query/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,17 @@ import {
Text,
TextField,
} from '@radix-ui/themes';
import { useMutation, useQuery } from '@tanstack/react-query';
import {
useInfiniteQuery,
useMutation,
useQuery,
useSuspenseInfiniteQuery,
} from '@tanstack/react-query';
import { useEffect, useState } from 'react';

import {
addPetMutation,
findPetsByTagsInfiniteOptions,
getPetByIdOptions,
updatePetMutation,
} from './client/@tanstack/react-query.gen';
Expand Down Expand Up @@ -90,6 +96,28 @@ function App() {
enabled: Boolean(petId),
});

useInfiniteQuery({
...findPetsByTagsInfiniteOptions({
client: localClient,
query: {
tags: [],
},
}),
getNextPageParam: (lastPage) => lastPage.map(({ name }) => name),
initialPageParam: { query: { tags: [] } },
});

useSuspenseInfiniteQuery({
...findPetsByTagsInfiniteOptions({
client: localClient,
query: {
tags: [],
},
}),
getNextPageParam: (lastPage) => lastPage.map(({ name }) => name),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What would happen if you omit this line? Would the types not complain? Would the runtime code likely crash?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

like below

_______________________________2026-01-27______________9 14 12

In the original implementation, even if we omitted getNextPageParam, TypeScript didn’t complain either

_______________________________2026-01-27______________9 13 56

initialPageParam: { query: { tags: [] } },
Copy link
Member

@mrlubos mrlubos Jan 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The other reason this smells is if you place this line BEFORE the generated options, it won't get applied. I think we either want to keep options as they were (not setting this property) or require this property if it's needed

});

const onAddPet = async (formData: FormData) => {
// simple form field validation to demonstrate using schemas
if (PetSchema.required.includes('name') && !formData.get('name')) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

import {
type DefaultError,
type InfiniteData,
infiniteQueryOptions,
queryOptions,
type UseMutationOptions,
} from '@tanstack/react-query';
Expand Down Expand Up @@ -195,6 +197,91 @@ export const findPetsByTagsOptions = (options: Options<FindPetsByTagsData>) =>
queryKey: findPetsByTagsQueryKey(options),
});

const createInfiniteParams = <
K extends Pick<QueryKey<Options>[0], 'body' | 'headers' | 'path' | 'query'>,
>(
queryKey: QueryKey<Options>,
page: K,
) => {
const params = { ...queryKey[0] };
if (page.body) {
params.body = {
...(queryKey[0].body as any),
...(page.body as any),
};
}
if (page.headers) {
params.headers = {
...queryKey[0].headers,
...page.headers,
};
}
if (page.path) {
params.path = {
...(queryKey[0].path as any),
...(page.path as any),
};
}
if (page.query) {
params.query = {
...(queryKey[0].query as any),
...(page.query as any),
};
}
return params as unknown as typeof page;
};

export const findPetsByTagsInfiniteQueryKey = (
options: Options<FindPetsByTagsData>,
): QueryKey<Options<FindPetsByTagsData>> =>
createQueryKey('findPetsByTags', options, true);

/**
* Finds Pets by tags.
*
* Multiple tags can be provided with comma separated strings. Use tag1, tag2, tag3 for testing.
*/
export const findPetsByTagsInfiniteOptions = (
options: Options<FindPetsByTagsData>,
) =>
infiniteQueryOptions<
FindPetsByTagsResponse,
DefaultError,
InfiniteData<FindPetsByTagsResponse>,
QueryKey<Options<FindPetsByTagsData>>,
| Array<string>
| Pick<
QueryKey<Options<FindPetsByTagsData>>[0],
'body' | 'headers' | 'path' | 'query'
>
>({
getNextPageParam: (() => {}) as any,
initialPageParam: {} as any,
queryFn: async ({ pageParam, queryKey, signal }) => {
// @ts-ignore
const page: Pick<
QueryKey<Options<FindPetsByTagsData>>[0],
'body' | 'headers' | 'path' | 'query'
> =
typeof pageParam === 'object'
? pageParam
: {
query: {
tags: pageParam,
},
};
const params = createInfiniteParams(queryKey, page);
const { data } = await Sdk.__registry.get().findPetsByTags({
...options,
...params,
signal,
throwOnError: true,
});
return data;
},
queryKey: findPetsByTagsInfiniteQueryKey(options),
});

/**
* Deletes a pet.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -260,7 +260,8 @@ export const createInfiniteQueryOptions = ({
.call(
$.object()
.pretty()
.hint('@ts-ignore')
.prop('initialPageParam', $.object().pretty().as('any'))
.prop('getNextPageParam', $.func().as('any'))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This feels very dirty. Is the issue that the inferred type doesn't contain initialPageParam and getNextPageParam? Why not add a second argument and require them?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not add a second argument and require them?

Could you give me code example please? I need more detail

.prop(
'queryFn',
$.func()
Expand Down
12 changes: 8 additions & 4 deletions packages/openapi-ts/src/ts-dsl/expr/as.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,14 @@ export class AsTsDsl extends Mixed {
}

override toAst() {
return ts.factory.createAsExpression(
this.$node(this.expr),
this.$type(this.type),
);
const exprNode = this.$node(this.expr);
// Wrap function expressions in parentheses to ensure correct precedence
// e.g., (() => {}) as any instead of () => {} as any
const wrappedExpr =
ts.isArrowFunction(exprNode) || ts.isFunctionExpression(exprNode)
? ts.factory.createParenthesizedExpression(exprNode)
: exprNode;
return ts.factory.createAsExpression(wrappedExpr, this.$type(this.type));
}
}

Expand Down
Loading