diff --git a/.changeset/gold-worms-wave.md b/.changeset/gold-worms-wave.md new file mode 100644 index 000000000..e8b5f3064 --- /dev/null +++ b/.changeset/gold-worms-wave.md @@ -0,0 +1,6 @@ +--- +"openapi-typescript-helpers": patch +"openapi-fetch": patch +--- + +Fix data/error discrimination when there are empty-body errors diff --git a/packages/openapi-fetch/src/index.d.ts b/packages/openapi-fetch/src/index.d.ts index e914ed77a..75bbb0054 100644 --- a/packages/openapi-fetch/src/index.d.ts +++ b/packages/openapi-fetch/src/index.d.ts @@ -1,6 +1,7 @@ import type { ErrorResponse, FilterKeys, + GetValueWithDefault, HasRequiredKeys, HttpMethod, MediaType, @@ -114,7 +115,11 @@ export type FetchOptions = RequestOptions & export type FetchResponse = | { data: ParseAsResponse< - FilterKeys>, Media>, + GetValueWithDefault< + SuccessResponse>, + Media, + Record + >, O >; error?: never; @@ -122,7 +127,11 @@ export type FetchResponse = } | { data?: never; - error: FilterKeys>, Media>; + error: GetValueWithDefault< + ErrorResponse>, + Media, + Record + >; response: Response; }; diff --git a/packages/openapi-fetch/test/fixtures/api.d.ts b/packages/openapi-fetch/test/fixtures/api.d.ts index 1ab3518ac..d37bd2e9f 100644 --- a/packages/openapi-fetch/test/fixtures/api.d.ts +++ b/packages/openapi-fetch/test/fixtures/api.d.ts @@ -24,6 +24,7 @@ export interface paths { }; responses: { 200: components["responses"]["AllPostsGet"]; + 401: components["responses"]["EmptyError"]; 500: components["responses"]["Error"]; }; }; @@ -457,6 +458,10 @@ export interface components { "text/html": string; }; }; + EmptyError: { + content: { + }; + }; Error: { content: { "application/json": { diff --git a/packages/openapi-fetch/test/fixtures/api.yaml b/packages/openapi-fetch/test/fixtures/api.yaml index 18afb82da..0310d7532 100644 --- a/packages/openapi-fetch/test/fixtures/api.yaml +++ b/packages/openapi-fetch/test/fixtures/api.yaml @@ -28,6 +28,8 @@ paths: responses: 200: $ref: '#/components/responses/AllPostsGet' + 401: + $ref: '#/components/responses/EmptyError' 500: $ref: '#/components/responses/Error' put: @@ -623,6 +625,8 @@ components: text/html: schema: type: string + EmptyError: + content: {} Error: content: application/json: diff --git a/packages/openapi-fetch/test/index.test-d.ts b/packages/openapi-fetch/test/index.test-d.ts new file mode 100644 index 000000000..943135c76 --- /dev/null +++ b/packages/openapi-fetch/test/index.test-d.ts @@ -0,0 +1,30 @@ +import { test, expectTypeOf } from "vitest"; + +import createClient from "../src/index.js"; +import type { paths } from "./fixtures/api.js"; + +const { GET } = createClient(); + +interface Blogpost { + title: string; + body: string; + publish_date?: number | undefined; +} + +// This is a type test that will not be executed +// eslint-disable-next-line vitest/expect-expect +test("the error type works properly", async () => { + const value = await GET("/blogposts"); + + if (value.data) { + expectTypeOf(value.data).toEqualTypeOf>(); + } else { + expectTypeOf(value.data).toBeUndefined(); + expectTypeOf(value.error) + .extract<{ code: number }>() + .toEqualTypeOf<{ code: number; message: string }>(); + expectTypeOf(value.error) + .exclude<{ code: number }>() + .toEqualTypeOf>(); + } +}); diff --git a/packages/openapi-typescript-helpers/index.d.ts b/packages/openapi-typescript-helpers/index.d.ts index fc1df928c..e4215e458 100644 --- a/packages/openapi-typescript-helpers/index.d.ts +++ b/packages/openapi-typescript-helpers/index.d.ts @@ -88,6 +88,9 @@ export type RequestBodyJSON = JSONLike< /** Find first match of multiple keys */ export type FilterKeys = Obj[keyof Obj & Matchers]; +/** Get the type of a value of an input object with a given key. If the key is not found, return a default type. Works with unions of objects too. */ +export type GetValueWithDefault = Obj extends any ? (FilterKeys extends never ? Default : FilterKeys) : never; + /** Return any `[string]/[string]` media type (important because openapi-fetch allows any content response, not just JSON-like) */ export type MediaType = `${string}/${string}`; /** Return any media type containing "json" (works for "application/json", "application/vnd.api+json", "application/vnd.oai.openapi+json") */