Skip to content

solid-query client: emitted code is uncompilable against @tanstack/solid-query v5 (mutations) #3365

Description

@mhamri

Description

Versions

Package Version
orval 8.10.0
@orval/query (bundled with orval@8.10.0)
@tanstack/solid-query 5.100.8
typescript 5.9.3

Summary

With output.client: 'solid-query' against @tanstack/solid-query@^5.71.5 (the
use*-prefix regime), every OpenAPI operation that becomes a mutation produces
generated code that fails type-check on two independent points:

  1. SolidMutationOptions is not exported by @tanstack/solid-query v5.
    Orval emits import type { …, SolidMutationOptions, … } from '@tanstack/solid-query',
    which fails with TS2724: '"@tanstack/solid-query"' has no exported member named 'SolidMutationOptions'. Did you mean 'MutationOptions'? on every
    generated file containing a mutation.

  2. mutation?: UseMutationOptions<…> is typed as a function but used as an
    object.
    In @tanstack/solid-query v5,
    UseMutationOptions<TData, TError, TVariables, TOnMutateResult> is declared as
    Accessor<MutationOptions<TData, TError, TVariables, TOnMutateResult>> — i.e.
    a getter function, not the options object. The emitted runtime spreads
    it as a plain object ({...options.mutation}) and the intended call site is
    usePutXxx({ mutation: { onSuccess, … } }). TypeScript rejects that with
    TS2353: Object literal may only specify known properties, and 'onSuccess' does not exist in type 'UseMutationOptions<…>'.

Both stem from getSolidQueryImports / createSolidAdapter in
@orval/query referring to type aliases that solid-query v5 no longer exports
under the same name (or with the same shape).

Reproduction

A minimal repro: any spec with one non-GET endpoint + an orval.config.ts like:

import { defineConfig } from 'orval';

export default defineConfig({
  example: {
    input: { target: './openapi.json' },
    output: {
      target: './src/api/generated',
      mode: 'tags-split',
      client: 'solid-query',
      httpClient: 'axios',
      override: {
        mutator: { path: './src/lib/client.ts', name: 'client' },
        // Leave query.useQuery / query.useMutation unset — Orval's per-verb
        // defaults are correct.
        query: { signal: true },
      },
    },
  },
});

@tanstack/solid-query installed at any version ≥ 5.71.5 (so the emitter
takes the use-prefix branch).

Run orval and then tsc --noEmit:

src/api/generated/example/example.ts:17:3 - error TS2724:
  '"@tanstack/solid-query"' has no exported member named 'SolidMutationOptions'.
  Did you mean 'MutationOptions'?

src/api/generated/example/example.ts:76:7 - error TS2353:
  Object literal may only specify known properties, and 'onSuccess' does
  not exist in type 'UseMutationOptions<OrderChangedEvent, ProblemDetails,
  {data: SomeBody}, unknown>'.

What the emitter produces (excerpt)

// AUTO-GENERATED by orval@8.10.0
import {
  useMutation,
  useQuery
} from '@tanstack/solid-query';
import type {
  // …
  SolidMutationOptions,            // ← does not exist in solid-query@5
  UseMutationOptions,              // ← solid-query@5: Accessor<MutationOptions<...>>
  UseMutationResult,
  // …
} from '@tanstack/solid-query';

export const getPutXxxMutationOptions = <TError = ProblemDetails, TContext = unknown>(
  options?: { mutation?: UseMutationOptions<, , { data: SomeBody }, TContext> },
): SolidMutationOptions<, , { data: SomeBody }, TContext> => {

  const { mutation: mutationOptions } = options ?
    /* … */
    : { mutation: { mutationKey, } };

  // mutationOptions is typed as Accessor<MutationOptions> (a function) but
  // spread here as an object:
  return { mutationFn, ...mutationOptions }
}

export const usePutXxx = <TError = ProblemDetails, TContext = unknown>(
  options?: { mutation?: UseMutationOptions<> },
  queryClient?: () => QueryClient,
): UseMutationResult<> => {
  return useMutation(() => getPutXxxMutationOptions(options), queryClient);
}

Pinpointing the bug

@orval/query/dist/index.mjs (v8.10.0):

  • Line 707–741 — getSolidQueryImports(prefix): hardcodes
    { name: "SolidMutationOptions" } in the import list regardless of prefix.
  • Line 805–811 — getSolidQueryDependencies: passes "use" when
    isSolidQueryWithUsePrefix(packageJson) (solid-query ≥ 5.71.5) is true, but
    the dependencies list still references SolidMutationOptions.
  • Line 861–866 — isSolidQueryWithUsePrefix: detects the prefix regime
    via compareVersions(version, "5.71.5").
  • Line 1113–1128 — createSolidAdapter / getOptionsReturnTypeName:
    unconditionally returns "SolidMutationOptions" for type === "mutation".

The use-prefix migration that landed in solid-query 5.71.5 dropped the
Solid*Options aliases. The emitter wasn't updated to match.

Expected behaviour

For @tanstack/solid-query@^5.71.5 the emitter should produce code that:

  1. Imports types that solid-query v5 actually re-exports. For the mutation
    options return type that means MutationOptions (re-exported from
    @tanstack/query-core).
  2. Types the user-facing mutation?: field as the plain
    MutationOptions<…> object, matching the way the emitted runtime spreads
    it (and the way every example in the docs invokes the hook with { mutation: { onSuccess, … } }).

Equivalent generated code:

import type {
  // …
  MutationOptions,        // ← real solid-query@5 export
  UseMutationOptions,     // ← keep if still used for Accessor positions
  UseMutationResult,
} from '@tanstack/solid-query';

export const getPutXxxMutationOptions = <TError, TContext>(
  options?: { mutation?: MutationOptions<> },
): MutationOptions<> => { /* … */ };

export const usePutXxx = <TError, TContext>(
  options?: { mutation?: MutationOptions<> },
  queryClient?: () => QueryClient,
): UseMutationResult<> => {
  return useMutation(() => getPutXxxMutationOptions(options), queryClient);
};

Suggested fix

Mechanical change in @orval/query:

  1. getSolidQueryImports — when the use-prefix branch is taken, replace
    { name: "SolidMutationOptions" } with { name: "MutationOptions" } (and
    the analogous swap for SolidQueryOptions / SolidInfiniteQueryOptions if
    they're affected the same way).
  2. createSolidAdapter.getOptionsReturnTypeName — when use-prefix is
    active, return "MutationOptions" instead of "SolidMutationOptions".
  3. Mutation options field type — switch the user-facing
    mutation?: UseMutationOptions<…> to mutation?: MutationOptions<…> so the
    call-site object literal validates. (This also matches the React Query
    emitter's behaviour where the field accepts an object, not an Accessor.)

Adding a small test fixture under samples/ that uses a non-GET operation
with client: 'solid-query' and @tanstack/solid-query@^5.71.5 and asserts
the output passes tsc would prevent the regression.

Current workaround (temporary)

Until this is fixed upstream, we apply a small post-processor as part of
api:gen that rewrites the affected files (idempotent, runs after orval):

  • drop SolidMutationOptions from imports + replace usages with
    MutationOptions
  • rewrite mutation?: UseMutationOptions<…>mutation?: MutationOptions<…>
  • ensure MutationOptions is added to the type-only import block

Happy to share the script and / or open a PR with the emitter fix if it's
useful.

Additional context

Spec uses [Consumes]-content-type dispatch on the .NET side, so
override.splitByContentType: true is set. Doesn't seem to interact with this
bug — the same break shows up on plain POST endpoints without content-type
splitting.

Output client

axios

Configuration (orval.config)

// orval.config.ts
import { defineConfig } from 'orval';

export default defineConfig({
  example: {
    input: {
      target: './openapi.json',
    },
    output: {
      target: './src/api/generated',
      mode: 'tags-split',
      client: 'solid-query',
      httpClient: 'axios',
      override: {
        splitByContentType: true,
        mutator: {
          path: './src/lib/api/client.ts',
          name: 'client',
        },
        // Leave query.useQuery / query.useMutation unset — Orval's per-verb
        // defaults (GET → useQuery, non-GET → useMutation) are correct.
        // Setting useQuery: true globally suppresses mutation emission via the
        // `if (verb !== GET && isQuery) isMutation = false` branch in
        // @orval/query/dist/index.mjs:2139.
        query: {
          signal: true,
        },
      },
    },
  },
});

Environment

System:
  OS: Windows 11 Pro 10.0.26200
  Shell: PowerShell 7 (pwsh)
Binaries:
  Node: 22.x (via Bun’s built-in)
  bun: 1.2.x
Package manager: bun (workspace)
npmPackages:
  orval: ^8.10.0 => 8.10.0
  @tanstack/solid-query: ^5.100.8 => 5.100.8
  @tanstack/solid-query-devtools: ^5.100.8 => 5.100.8
  solid-js: ^1.9.12 => 1.9.12
  typescript: ^5.9.3 => 5.9.3
  vite: ^6.4.2 => 6.4.2
  vite-plugin-solid: ^2.11.12 => 2.11.12
  axios: ^1.16.0 => 1.16.0

Filed from a Bun workspace; same break reproduces under pnpm / npm (the
emitter only consults package.json for the solid-query version, not the
package manager).

Expected behavior

Generated code for a mutation operation should compile under tsc --noEmit
against @tanstack/solid-query@^5.71.5. Specifically:

  1. The type-only import block must reference names that solid-query v5 actually
    re-exports — MutationOptions (plain TanStack-core object) at the return
    position of getXxxMutationOptions, not the non-existent
    SolidMutationOptions.
  2. The user-facing options.mutation parameter must be typed as the plain
    MutationOptions<…> object (matching the way the emitted runtime spreads
    it with {...options.mutation} and the way callers invoke the hook with
    usePutXxx({ mutation: { onSuccess, … } })), not UseMutationOptions<…>
    which in solid-query v5 is an Accessor<MutationOptions<…>> (a function).

Concretely, the emitter should produce:

import type {
  MutationOptions,
  UseMutationResult,
  // (UseMutationOptions only where an Accessor is genuinely required)
} from '@tanstack/solid-query';

export const getPutXxxMutationOptions = <TError, TContext>(
  options?: { mutation?: MutationOptions<, TError, { data: SomeBody }, TContext> },
): MutationOptions<, TError, { data: SomeBody }, TContext> => { /* … */ };

export const usePutXxx = <TError, TContext>(
  options?: { mutation?: MutationOptions<> },
  queryClient?: () => QueryClient,
): UseMutationResult<> => {
  return useMutation(() => getPutXxxMutationOptions(options), queryClient);
};

Actual behavior

Running orval against the config + spec below, then tsc --noEmit:

src/api/generated/example/example.ts:17:3 - error TS2724:
  '"@tanstack/solid-query"' has no exported member named 'SolidMutationOptions'.
  Did you mean 'MutationOptions'?

17     SolidMutationOptions,
       ~~~~~~~~~~~~~~~~~~~~

src/api/generated/example/example.ts:76:7 - error TS2353:
  Object literal may only specify known properties, and 'onSuccess' does not
  exist in type 'UseMutationOptions<OrderChangedEvent, ProblemDetails,
  { data: SomeBody; }, unknown>'.

76       onSuccess: (data, vars) => { /* … */ },
         ~~~~~~~~~

Every generated *.ts file containing at least one non-GET endpoint fails to
compile — for our internal POS-Web codebase that's 5 of 8 tag-split files
(branch, kitchen, me-kds-state, order, services).

If users try to recover by // @ts-expect-error-ing the import, the second
error still fires at every call site, and the {...options.mutation} spread
at runtime silently drops onSuccess / onMutate / onSettled because
spreading a function evaluates to {} — so lifecycle callbacks never fire.

OpenAPI document (minimal, if applicable)

openapi: 3.0.3
info:
  title: Demo
  version: 0.0.0
paths:
  /pets:
    post:
      operationId: createPet
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/Pet'
      responses:
        '201':
          description: Created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Pet'
components:
  schemas:
    Pet:
      type: object
      required: [id, name]
      properties:
        id:
          type: string
          format: uuid
        name:
          type: string

A single POST is sufficient to reproduce both TS2724 and TS2353. Adding a GET
alongside confirms that only the mutation-bearing operations fail — the
GET output remains clean.

Additional context

  • Workaround in use. A ~100-line post-processor running after orval in
    the api:gen script. Idempotent regex rewrite on every file under
    src/api/generated/:

    • drop SolidMutationOptions from the import type {…} block;
    • swap SolidMutationOptionsMutationOptions in type positions;
    • swap mutation?: UseMutationOptions<…>mutation?: MutationOptions<…>;
    • add MutationOptions to the type-only import block when needed.

    Happy to share the script if it'd help craft the upstream fix or a
    regression sample — it pins exactly where the emitter diverges from what
    solid-query v5 accepts.

  • Solid Query 5.71.5 changelog reference. The use*-prefix migration that
    triggers isSolidQueryWithUsePrefix (@orval/query/dist/index.mjs:861-866)
    appears to be the release that dropped the Solid*Options aliases from the
    package's public surface. Bisecting @tanstack/solid-query versions around
    5.71.x against this emitter would identify the exact pre-rename version
    that still works.

  • Related but distinct concern (separate issue worth opening). Setting
    output.override.query.useQuery: true globally — a config that looks
    natural for "I want useQuery hooks generated" — silently suppresses every
    useMutation hook. Root cause is the conflict resolver at
    @orval/query/dist/index.mjs:2131-2140:

    const effectiveUseQuery    =  ?? override.query.useQuery    ?? verb === Verbs.GET;
    const effectiveUseMutation =  ?? override.query.useMutation ?? verb !== Verbs.GET;
    let isQuery    = effectiveUseQuery || ;
    let isMutation = effectiveUseMutation && verb !== Verbs.GET;
    if (verb !== Verbs.GET && isQuery)    isMutation = false;   // ← suppresses
    if (verb === Verbs.GET && isMutation) isQuery = false;

    With useQuery: true set globally, isQuery becomes true for every verb,
    the conflict resolver forces isMutation = false, and the matching
    useMutation: true flag never gets a chance to fire. A docs warning (or a
    config-validation log) would have caught this for us much earlier — happy
    to file a separate issue if you'd prefer it tracked independently rather
    than as a footnote here.

  • Willing to PR. I can draft the emitter fix + a samples/solid-query-v5/
    fixture (one GET, one POST, asserts tsc --noEmit passes) if a maintainer
    confirms the direction in [Suggested fix].

Metadata

Metadata

Assignees

Labels

solidSolidJS or SolidStarttanstack-queryTanStack Query related issue

Type

Projects

No projects

Milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions