orval-labs / orval

orval is able to generate client with appropriate type-signatures (TypeScript) from any valid OpenAPI v3 or Swagger v2 specification, either in yaml or json formats. 🍺
https://orval.dev
MIT License
2.82k stars 314 forks source link

React query initialData overloads don't respect the allParamsOptional config option #1564

Open RinseV opened 1 month ago

RinseV commented 1 month ago

What are the steps to reproduce this issue?

  1. Enable the allParamsOptional option in the config
  2. Generate the hooks with the react-query client
  3. Try to use a hook with an optional parameter

OpenAPI sample spec:

openapi: '3.0.0'
info:
  version: 1.0.0
  title: Swagger Petstore
  license:
    name: MIT
servers:
  - url: http://petstore.swagger.io/v1
paths:
  /pets/{petId}:
    get:
      summary: Info for a specific pet
      operationId: pet
      tags:
        - pets
      parameters:
        - name: petId
          in: path
          required: true
          description: The id of the pet to retrieve
          schema:
            type: string
      responses:
        '200':
          description: Expected response to a valid request
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Pet'
        default:
          description: unexpected error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
components:
  schemas:
    Pet:
      type: object
      required:
        - id
        - name
      properties:
        id:
          type: integer
          format: int64
        name:
          type: string
        tag:
          type: string
        email:
          type: string
          format: email
        callingCode:
          type: string
          enum: ['+33', '+420', '+33'] # intentional duplicated value
        country:
          type: string
          enum: ["People's Republic of China", 'Uruguay']
    Error:
      type: object
      required:
        - code
        - message
      properties:
        code:
          type: integer
          format: int32
        message:
          type: string

orval.config.ts:

import { defineConfig } from 'orval';

export default defineConfig({
  allParamsOptional: {
    output: {
      target: 'src/api/endpoints.ts',
      schemas: 'src/api/model',
      client: 'react-query',
      httpClient: 'fetch',
      allParamsOptional: true,
    },
    input: {
      target: 'petstore.yaml'
    },
  },
})

What happens?

You get a Typescript error:

No overload matches this call.
  Overload 1 of 3, '(petId: string, options?: { query?: (Partial<UseQueryOptions<petResponse, Promise<Error>, petResponse, QueryKey>> & Pick<...>) | undefined; fetch?: RequestInit | undefined; } | undefined): UseQueryResult<...> & { ...; }', gave the following error.
    Argument of type 'string | undefined' is not assignable to parameter of type 'string'.
      Type 'undefined' is not assignable to type 'string'.
  Overload 2 of 3, '(petId: string, options?: { query?: Partial<UseQueryOptions<petResponse, Promise<Error>, petResponse, QueryKey>> | undefined; fetch?: RequestInit | undefined; } | undefined): UseQueryResult<...> & { ...; }', gave the following error.
    Argument of type 'string | undefined' is not assignable to parameter of type 'string'.
      Type 'undefined' is not assignable to type 'string'.ts(2769)
endpoints.ts(137, 17): The call would have succeeded against this implementation, but implementation signatures of overloads are not externally visible.

Generated overloads:

export function usePet<
  TData = Awaited<ReturnType<typeof pet>>,
  TError = Promise<Error>,
>(
  // Should be string | undefined | null
  petId: string,
  options: {
    query: Partial<
      UseQueryOptions<Awaited<ReturnType<typeof pet>>, TError, TData>
    > &
      Pick<
        DefinedInitialDataOptions<
          Awaited<ReturnType<typeof pet>>,
          TError,
          TData
        >,
        "initialData"
      >;
    fetch?: RequestInit;
  },
): DefinedUseQueryResult<TData, TError> & { queryKey: QueryKey };
export function usePet<
  TData = Awaited<ReturnType<typeof pet>>,
  TError = Promise<Error>,
>(
  // Should be string | undefined | null
  petId: string,
  options?: {
    query?: Partial<
      UseQueryOptions<Awaited<ReturnType<typeof pet>>, TError, TData>
    > &
      Pick<
        UndefinedInitialDataOptions<
          Awaited<ReturnType<typeof pet>>,
          TError,
          TData
        >,
        "initialData"
      >;
    fetch?: RequestInit;
  },
): UseQueryResult<TData, TError> & { queryKey: QueryKey };
export function usePet<
  TData = Awaited<ReturnType<typeof pet>>,
  TError = Promise<Error>,
>(
  // Should be string | undefined | null
  petId: string,
  options?: {
    query?: Partial<
      UseQueryOptions<Awaited<ReturnType<typeof pet>>, TError, TData>
    >;
    fetch?: RequestInit;
  },
): UseQueryResult<TData, TError> & { queryKey: QueryKey };
/**
 * @summary Info for a specific pet
 */

export function usePet<
  TData = Awaited<ReturnType<typeof pet>>,
  TError = Promise<Error>,
>(
  // The actual implementation has the correct types
  petId: string | undefined | null,
  options?: {
    query?: Partial<
      UseQueryOptions<Awaited<ReturnType<typeof pet>>, TError, TData>
    >;
    fetch?: RequestInit;
  },
): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
  const queryOptions = getPetQueryOptions(petId, options);

  const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & {
    queryKey: QueryKey;
  };

  query.queryKey = queryOptions.queryKey;

  return query;
}

What were you expecting to happen?

The optional parameter would be allowed in all overloads

Any other comments?

This PR added overloads to all hooks to allow for proper typing of the response when supplying initialData. However, these overloads don't consider the allParamsOptional option for the path parameters resulting in 3 function signatures that don't allow for undefined path parameters while the actual implementation does allow for it.

The problem is that these function overloads use the definition of the endpoint rather than the implementation and the allParamsOptional config setting only applies to the implementation of the endpoint's definition. However, the function overloads cannot use the implementation because the implementation also includes the default values which you cannot define in function overloads. We would need something that's in between the definition and implementation that does include the | undefined | null from the allParamsOptional setting but does not include the default parameters from the implementation.

What versions are you using?

System:
    OS: Linux 5.15 Ubuntu 20.04.6 LTS (Focal Fossa)
    CPU: (16) x64 AMD Ryzen 7 5800X 8-Core Processor
    Memory: 5.14 GB / 7.76 GB
    Container: Yes
    Shell: 5.8 - /usr/bin/zsh
  npmPackages:
    @tanstack/react-query: ^5.51.21 => 5.51.21 
    orval: ^7.0.1 => 7.0.1
melloware commented 1 month ago

PR is welcome.

cc @lnkarma