Skip to content

Fix type discrimination for non-overlapping content types #1610

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Apr 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading