Skip to content

feat(@tanstack/query-core): make query key functions accept optional …#3925

Open
AustinRobinson wants to merge 5 commits into
hey-api:mainfrom
AustinRobinson:feat/query-key-partial-options
Open

feat(@tanstack/query-core): make query key functions accept optional …#3925
AustinRobinson wants to merge 5 commits into
hey-api:mainfrom
AustinRobinson:feat/query-key-partial-options

Conversation

@AustinRobinson
Copy link
Copy Markdown

Make Query Key Functions Accept Optional Partial Options for TanStack Query Prefix Matching

Changes Made

Modified packages/openapi-ts/src/plugins/@tanstack/query-core/queryKey.ts line 165:

- .param('options', (p) => p.required(hasOperationDataRequired(operation)).type(typeData))
+ .param('options', (p) => p.optional().type(`Partial<${typeData}>`))

This changes query key function signatures from:

(options: Options<T>) => QueryKey

to:

(options?: Partial<Options<T>>) => QueryKey

Options functions remain strict to preserve type safety for queries.


Problem

When using TanStack Query's prefix matching feature for query invalidation (via exact: false), the generated query key functions require all parameters to be provided, making it difficult to invalidate queries by prefix without passing unnecessary parameters or resorting to manual string literals.

// Generated query key function requires all parameters
export const listTodosQueryKey = (
  options: Options<ListTodosData>
) => createQueryKey('listTodos', options);

// To invalidate all listTodos queries, I'm forced to:
// Option 1: Pass unnecessary parameters
queryClient.invalidateQueries({
  queryKey: listTodosQueryKey({ 
    query: { limit: 0, offset: 0 } // Don't care about these values!
  }),
  exact: false
});

// Option 2: Manually construct with string literals (loses type safety)
queryClient.invalidateQueries({
  queryKey: [{ _id: 'listTodos' }],
  exact: false
});

Issues:

  1. Cannot call query key functions without arguments
  2. Must pass unnecessary parameters or use string literals
  3. String literals aren't type-safe and won't catch API changes at compile time

Motivation

After mutations that affect multiple related queries, I need to invalidate all queries for a specific endpoint regardless of their parameters:

// After completing a todo, invalidate all todo queries
await completeTodo.mutateAsync({ id: '123' });

// Want to use the generated function:
queryClient.invalidateQueries({
  queryKey: listTodosQueryKey(), // Type-safe, no unnecessary params
  exact: false
});

Why this matters:

  • Type safety is the primary benefit of code generation
  • IDE autocomplete and refactoring should work for invalidation patterns
  • API changes should be caught at compile time, not runtime

Proposed Solution

Make query key functions accept optional partial options (options?: Partial<Options<T>>) while keeping options functions strict for type safety.

Key changes:

  1. Make the options parameter optional (add ?)
  2. Make the options type partial (wrap in Partial<>)

This allows calling the function with no arguments, partial arguments, or full arguments.

Current Generated Code

// Query key function - currently strict
export const getTodosByUserQueryKey = (
  options: Options<GetTodosByUserData>
) => createQueryKey('getTodosByUser', options);

// Options function - used with useQuery
export const getTodosByUserOptions = (
  options: Options<GetTodosByUserData>
) => {
  return queryOptions({
    queryFn: async ({ queryKey, signal }) => {
      const { data } = await getTodosByUser({
        ...options,
        ...queryKey[0],
        signal,
        throwOnError: true
      });
      return data;
    },
    queryKey: getTodosByUserQueryKey(options)
  });
};

Proposed Change

// Query key function - NOW ACCEPTS OPTIONAL PARTIAL OPTIONS
export const getTodosByUserQueryKey = (
  options?: Partial<Options<GetTodosByUserData>>  // Optional AND Partial
) => createQueryKey('getTodosByUser', options);

// Options function - STAYS STRICT (no changes needed)
export const getTodosByUserOptions = (
  options: Options<GetTodosByUserData>  // Still requires all params!
) => {
  return queryOptions({
    queryFn: async ({ queryKey, signal }) => {
      const { data } = await getTodosByUser({
        ...options,
        ...queryKey[0],
        signal,
        throwOnError: true
      });
      return data;
    },
    queryKey: getTodosByUserQueryKey(options)  // Full options work with Partial
  });
};

Why This Works

Type Safety is Preserved for Queries

The options function (used with useQuery) remains strict:

// TypeScript enforces all required params
const { data } = useQuery(
  getTodosByUserOptions({
    path: { userId: '123' },
    query: { status: 'active', limit: 10, offset: 0 }
  })
);

// TypeScript error: missing required params
const { data } = useQuery(
  getTodosByUserOptions({
    path: { userId: '123' }  // Error!
  })
);

Flexibility for Invalidation

The query key function accepts no arguments, partial arguments, or full arguments:

// Invalidate all queries for a specific user
queryClient.invalidateQueries({
  queryKey: getTodosByUserQueryKey({ path: { userId: '123' } }),
  exact: false
});

// Invalidate ALL queries of this type
queryClient.invalidateQueries({
  queryKey: getTodosByUserQueryKey(),
  exact: false
});

Backward Compatible

Existing code passing full options continues to work (full type is assignable to Partial<T>).

Benefits

  • Type-safe query invalidation without string literals
  • Call with no arguments: listTodosQueryKey()
  • Backward compatible: all existing code works
  • Simple implementation: only function signatures change
  • Better DX: IDE autocomplete and refactoring work correctly

Alternative Considered

Exporting Operation ID Constants

Instead of making query key functions accept partial options, we could export constants for each operation ID:

// Generated constants
export const QUERY_IDS = {
  listTodos: 'listTodos',
  getTodosByUser: 'getTodosByUser',
  // ...
} as const;

// Query key functions remain strict
export const listTodosQueryKey = (options: Options<ListTodosData>) => 
  createQueryKey(QUERY_IDS.listTodos, options);

Usage:

// Users would manually construct query keys using the constants
queryClient.invalidateQueries({
  queryKey: [{ _id: QUERY_IDS.listTodos }],
  exact: false
});

Why this is less ideal:

While this approach would solve the type safety issue, it has several drawbacks:

  1. Requires more code generation: Need to generate both constants and functions (doubles exports)
  2. Exposes implementation details: Users need to understand how createQueryKey structures the query key with _id
  3. Manual query key construction: Users must know to wrap the constant in [{ _id: ... }] format
  4. Less ergonomic: More verbose than simply calling listTodosQueryKey()
  5. Inconsistent patterns: Mixes manual construction with generated functions
  6. Doesn't leverage existing functions: The generated query key functions still can't be used for prefix matching

The proposed solution is superior because it:

  • Requires minimal changes (only function signatures)
  • No additional exports needed
  • Users don't need to understand internal query key structure
  • More intuitive: just call the function with no/partial arguments
  • Leverages existing generated functions

Testing

  • ✅ TypeScript typecheck passes for TanStack Query v5 tests
  • ✅ Build succeeds
  • ✅ Generated code is syntactically correct
  • ⚠️ Snapshot tests need updating (expected - signatures changed)

Snapshot Tests

The snapshot tests show expected failures because the generated query key function signatures have changed. The snapshots will need to be updated with pnpm tu @test/openapi-ts-tanstack-query-v5 to reflect the new options?: Partial<...> signatures.

Related Issues

Resolves #980, #1682, #3789

Configuration (Optional)

If desired, this could be made configurable in the future:

{
  plugins: [
    {
      name: '@tanstack/react-query',
      queryKeyPartial: true, // default: true
    }
  ]
}

…partial options

This change allows query key functions to be called with no arguments,
partial arguments, or full arguments, enabling type-safe prefix matching
for query invalidation with TanStack Query's exact: false option.

- Changed options parameter from required to optional
- Wrapped options type in Partial<> to allow partial arguments
- Options functions remain strict to preserve type safety for queries
- Backward compatible: existing code passing full options continues to work

Resolves hey-api#980, hey-api#1682, hey-api#3789
@bolt-new-by-stackblitz
Copy link
Copy Markdown

Review PR in StackBlitz Codeflow Run & review this pull request in StackBlitz Codeflow.

@vercel
Copy link
Copy Markdown

vercel Bot commented May 24, 2026

@AustinRobinson is attempting to deploy a commit to the Hey API Team on Vercel.

A member of the Team first needs to authorize it.

@dosubot dosubot Bot added the size:XS This PR changes 0-9 lines, ignoring generated files. label May 24, 2026
@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented May 24, 2026

🦋 Changeset detected

Latest commit: 17bd9da

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@hey-api/openapi-ts Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@dosubot dosubot Bot added the feature 🚀 Feature request. label May 24, 2026
Copy link
Copy Markdown
Contributor

@pullfrog pullfrog Bot left a comment

Choose a reason for hiding this comment

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

Important

This PR introduces a type error for infinite query key functions. The createQueryKey utility needs its parameter type updated to match the new partial query key signatures.

Reviewed changes — make generated TanStack Query query key functions accept optional partial options for prefix matching.

  • Change query key parameter type — from options: Options<T> to options?: Partial<Options<T>> in queryKeyStatement
  • Remove unused hasOperationDataRequired import — no longer needed after the parameter change

⚠️ Infinite query key functions will fail to typecheck

The queryKeyStatement change makes query key functions pass Partial<Options<T>> to createQueryKey, but createQueryKey still declares its parameter as options?: TOptions. For infinite queries, the explicit return type QueryKey<Options<T>> creates a mismatch: TypeScript infers TOptions = Partial<Options<T>> from the argument, producing return type QueryKey<Partial<Options<T>>>, which is not assignable to the annotated QueryKey<Options<T>>.

Technical details
# Infinite query key functions will fail to typecheck

## Affected sites
- `packages/openapi-ts/src/plugins/@tanstack/query-core/queryKey.ts:49``createQueryKeyFunction` parameter type

## Required outcome
- Infinite query key functions must compile without type errors after the PR
- Regular query key functions must continue to work as intended

## Suggested approach
Update `createQueryKeyFunction` in `queryKey.ts` line 49:

```typescript
// Before
.param('options', (p) => p.optional().type(TOptionsType))

// After
.param('options', (p) => p.optional().type(`Partial<${TOptionsType}>`))
```

This allows `createQueryKey` to accept partial options while inferring the full `TOptions` type from contextual return types (e.g., the explicit `QueryKey<Options<TData>>` on infinite query key functions).

Pullfrog  | Fix it ➔View workflow run | Using Kimi K2𝕏

@dosubot dosubot Bot added size:M This PR changes 30-99 lines, ignoring generated files. and removed size:XS This PR changes 0-9 lines, ignoring generated files. labels May 24, 2026
@dosubot dosubot Bot added size:L This PR changes 100-499 lines, ignoring generated files. and removed size:M This PR changes 30-99 lines, ignoring generated files. labels May 24, 2026
@codecov
Copy link
Copy Markdown

codecov Bot commented May 24, 2026

Codecov Report

❌ Patch coverage is 0% with 4 lines in your changes missing coverage. Please review.
✅ Project coverage is 38.77%. Comparing base (d5ba55b) to head (17bd9da).
⚠️ Report is 2 commits behind head on main.

Files with missing lines Patch % Lines
...pi-ts/src/plugins/@tanstack/query-core/queryKey.ts 0.00% 4 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #3925      +/-   ##
==========================================
- Coverage   38.77%   38.77%   -0.01%     
==========================================
  Files         595      595              
  Lines       21365    21367       +2     
  Branches     6298     6298              
==========================================
  Hits         8284     8284              
- Misses      10659    10661       +2     
  Partials     2422     2422              
Flag Coverage Δ
unittests 38.77% <0.00%> (-0.01%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new Bot commented May 24, 2026

Open in StackBlitz

@hey-api/codegen-core

npm i https://pkg.pr.new/@hey-api/codegen-core@3925

@hey-api/json-schema-ref-parser

npm i https://pkg.pr.new/@hey-api/json-schema-ref-parser@3925

@hey-api/nuxt

npm i https://pkg.pr.new/@hey-api/nuxt@3925

@hey-api/openapi-ts

npm i https://pkg.pr.new/@hey-api/openapi-ts@3925

@hey-api/shared

npm i https://pkg.pr.new/@hey-api/shared@3925

@hey-api/spec-types

npm i https://pkg.pr.new/@hey-api/spec-types@3925

@hey-api/types

npm i https://pkg.pr.new/@hey-api/types@3925

@hey-api/vite-plugin

npm i https://pkg.pr.new/@hey-api/vite-plugin@3925

commit: 17bd9da

@AustinRobinson
Copy link
Copy Markdown
Author

@mrlubos Just wanted to follow up on this PR and see whether it’s something you’d be interested in merging. If there are any changes, adjustments, or additional context you’d like before reviewing, I’d be happy to update it. Thanks!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feature 🚀 Feature request. size:L This PR changes 100-499 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

How to queryClient.invalidateQueries in hey-api/openapi-ts

1 participant