Skip to content

Commit

Permalink
Fix type discrimination for non-overlapping content types (#1610)
Browse files Browse the repository at this point in the history
* Add a failing type test for `openapi-fetch`

* Actually fix the issue

* Fix lint errors and add a changeset
  • Loading branch information
illright committed Apr 18, 2024
1 parent f6d062c commit cc8073b
Show file tree
Hide file tree
Showing 6 changed files with 59 additions and 2 deletions.
6 changes: 6 additions & 0 deletions .changeset/gold-worms-wave.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"openapi-typescript-helpers": patch
"openapi-fetch": patch
---

Fix data/error discrimination when there are empty-body errors
13 changes: 11 additions & 2 deletions packages/openapi-fetch/src/index.d.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type {
ErrorResponse,
FilterKeys,
GetValueWithDefault,
HasRequiredKeys,
HttpMethod,
MediaType,
Expand Down Expand Up @@ -114,15 +115,23 @@ export type FetchOptions<T> = RequestOptions<T> &
export type FetchResponse<T, O, Media extends MediaType> =
| {
data: ParseAsResponse<
FilterKeys<SuccessResponse<ResponseObjectMap<T>>, Media>,
GetValueWithDefault<
SuccessResponse<ResponseObjectMap<T>>,
Media,
Record<string, never>
>,
O
>;
error?: never;
response: Response;
}
| {
data?: never;
error: FilterKeys<ErrorResponse<ResponseObjectMap<T>>, Media>;
error: GetValueWithDefault<
ErrorResponse<ResponseObjectMap<T>>,
Media,
Record<string, never>
>;
response: Response;
};

Expand Down
5 changes: 5 additions & 0 deletions packages/openapi-fetch/test/fixtures/api.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ export interface paths {
};
responses: {
200: components["responses"]["AllPostsGet"];
401: components["responses"]["EmptyError"];
500: components["responses"]["Error"];
};
};
Expand Down Expand Up @@ -457,6 +458,10 @@ export interface components {
"text/html": string;
};
};
EmptyError: {
content: {
};
};
Error: {
content: {
"application/json": {
Expand Down
4 changes: 4 additions & 0 deletions packages/openapi-fetch/test/fixtures/api.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ paths:
responses:
200:
$ref: '#/components/responses/AllPostsGet'
401:
$ref: '#/components/responses/EmptyError'
500:
$ref: '#/components/responses/Error'
put:
Expand Down Expand Up @@ -623,6 +625,8 @@ components:
text/html:
schema:
type: string
EmptyError:
content: {}
Error:
content:
application/json:
Expand Down
30 changes: 30 additions & 0 deletions packages/openapi-fetch/test/index.test-d.ts
Original file line number Diff line number Diff line change
@@ -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<paths>();

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<Array<Blogpost>>();
} else {
expectTypeOf(value.data).toBeUndefined();
expectTypeOf(value.error)
.extract<{ code: number }>()
.toEqualTypeOf<{ code: number; message: string }>();
expectTypeOf(value.error)
.exclude<{ code: number }>()
.toEqualTypeOf<Record<string, never>>();
}
});
3 changes: 3 additions & 0 deletions packages/openapi-typescript-helpers/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,9 @@ export type RequestBodyJSON<PathMethod> = JSONLike<

/** Find first match of multiple keys */
export type FilterKeys<Obj, Matchers> = 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, KeyPattern, Default> = Obj extends any ? (FilterKeys<Obj, KeyPattern> extends never ? Default : FilterKeys<Obj, KeyPattern>) : 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") */
Expand Down

0 comments on commit cc8073b

Please sign in to comment.