Skip to content

openapi-fetch: add discriminant property to FetchResponse #2071

Closed
@OliverJAsh

Description

@OliverJAsh

Consider the following code:

import createFetchClient from "openapi-fetch";
import type { paths } from "./schema";

const fetchClient = createFetchClient<paths>({
  baseUrl: "https://httpbin.org",
});

const run = async () => {
  const result = await fetchClient.GET("/status/{code}", {
    params: { path: { code: 500 } },
  });
  const { data, error } = result;

  // ❌ This will be evaluated as `false`.
  if (error) {
    console.log("error", error);
  } else {
    // ❌
    // TypeScript has narrowed `data` to a non-nullable type.
    // But, at runtime, `data` is `undefined`.
    console.log("data", data);
  }
};

run();

In this example, openapi-fetch will return an error of value undefined, because content length is 0 and the response is not okay:

// handle empty content
if (response.status === 204 || response.headers.get("Content-Length") === "0") {
return response.ok ? { data: undefined, response } : { error: undefined, response };
}

When this happens, the if (error) branch above will be evaluated as false, despite the fact there was actually an error (albeit undefined). (This is also a problem in openapi-react-query.)

TypeScript will then narrow the type of data to a non-nullable type (i.e. exclude undefined). But at runtime, data is actually undefined.

We can mitigate this by avoiding destructuring and using the in operator like so:

const run = async () => {
  const result = await fetchClient.GET("/status/{code}", {
    params: { path: { code: 500 } },
  });

  // ✅ This will be evaluated as `true`.
  if ("error" in result) {
    // ✅
    console.log("error", result.error);
  } else {
    console.log("data", result.data);
  }
};

However, it's a shame to lose destructuring. For this reason I wanted to suggest that openapi-fetch could return a discriminant property in FetchResponse, e.g.:

 export type FetchResponse<T extends Record<string | number, any>, Options, Media extends MediaType> =
   | {
+      responseOk: true;
       data: ParseAsResponse<SuccessResponse<ResponseObjectMap<T>, Media>, Options>;
       error?: never;
       response: Response;
     }
     | {
+      responseOk: false;
       data?: never;
       error: ErrorResponse<ResponseObjectMap<T>, Media>;
       response: Response;
     };

Then, destructuring would be safe again:

const run = async () => {
  const result = await fetchClient.GET("/status/{code}", {
    params: { path: { code: 500 } },
  });

  const { data, error, responseOk } = result;

  // ✅ This will be evaluated as `true`.
  if (!responseOk) {
    // ✅
    console.log("error", error);
  } else {
    console.log("data", data);
  }
};

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or requestopenapi-fetchRelevant to the openapi-fetch library

    Type

    No type

    Projects

    Status

    Done

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions