Skip to content

Commit cc8073b

Browse files
authored
Fix type discrimination for non-overlapping content types (#1610)
* Add a failing type test for `openapi-fetch` * Actually fix the issue * Fix lint errors and add a changeset
1 parent f6d062c commit cc8073b

File tree

6 files changed

+59
-2
lines changed

6 files changed

+59
-2
lines changed

.changeset/gold-worms-wave.md

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"openapi-typescript-helpers": patch
3+
"openapi-fetch": patch
4+
---
5+
6+
Fix data/error discrimination when there are empty-body errors

packages/openapi-fetch/src/index.d.ts

+11-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import type {
22
ErrorResponse,
33
FilterKeys,
4+
GetValueWithDefault,
45
HasRequiredKeys,
56
HttpMethod,
67
MediaType,
@@ -114,15 +115,23 @@ export type FetchOptions<T> = RequestOptions<T> &
114115
export type FetchResponse<T, O, Media extends MediaType> =
115116
| {
116117
data: ParseAsResponse<
117-
FilterKeys<SuccessResponse<ResponseObjectMap<T>>, Media>,
118+
GetValueWithDefault<
119+
SuccessResponse<ResponseObjectMap<T>>,
120+
Media,
121+
Record<string, never>
122+
>,
118123
O
119124
>;
120125
error?: never;
121126
response: Response;
122127
}
123128
| {
124129
data?: never;
125-
error: FilterKeys<ErrorResponse<ResponseObjectMap<T>>, Media>;
130+
error: GetValueWithDefault<
131+
ErrorResponse<ResponseObjectMap<T>>,
132+
Media,
133+
Record<string, never>
134+
>;
126135
response: Response;
127136
};
128137

packages/openapi-fetch/test/fixtures/api.d.ts

+5
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ export interface paths {
2424
};
2525
responses: {
2626
200: components["responses"]["AllPostsGet"];
27+
401: components["responses"]["EmptyError"];
2728
500: components["responses"]["Error"];
2829
};
2930
};
@@ -457,6 +458,10 @@ export interface components {
457458
"text/html": string;
458459
};
459460
};
461+
EmptyError: {
462+
content: {
463+
};
464+
};
460465
Error: {
461466
content: {
462467
"application/json": {

packages/openapi-fetch/test/fixtures/api.yaml

+4
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ paths:
2828
responses:
2929
200:
3030
$ref: '#/components/responses/AllPostsGet'
31+
401:
32+
$ref: '#/components/responses/EmptyError'
3133
500:
3234
$ref: '#/components/responses/Error'
3335
put:
@@ -623,6 +625,8 @@ components:
623625
text/html:
624626
schema:
625627
type: string
628+
EmptyError:
629+
content: {}
626630
Error:
627631
content:
628632
application/json:
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { test, expectTypeOf } from "vitest";
2+
3+
import createClient from "../src/index.js";
4+
import type { paths } from "./fixtures/api.js";
5+
6+
const { GET } = createClient<paths>();
7+
8+
interface Blogpost {
9+
title: string;
10+
body: string;
11+
publish_date?: number | undefined;
12+
}
13+
14+
// This is a type test that will not be executed
15+
// eslint-disable-next-line vitest/expect-expect
16+
test("the error type works properly", async () => {
17+
const value = await GET("/blogposts");
18+
19+
if (value.data) {
20+
expectTypeOf(value.data).toEqualTypeOf<Array<Blogpost>>();
21+
} else {
22+
expectTypeOf(value.data).toBeUndefined();
23+
expectTypeOf(value.error)
24+
.extract<{ code: number }>()
25+
.toEqualTypeOf<{ code: number; message: string }>();
26+
expectTypeOf(value.error)
27+
.exclude<{ code: number }>()
28+
.toEqualTypeOf<Record<string, never>>();
29+
}
30+
});

packages/openapi-typescript-helpers/index.d.ts

+3
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,9 @@ export type RequestBodyJSON<PathMethod> = JSONLike<
8888

8989
/** Find first match of multiple keys */
9090
export type FilterKeys<Obj, Matchers> = Obj[keyof Obj & Matchers];
91+
/** 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. */
92+
export type GetValueWithDefault<Obj, KeyPattern, Default> = Obj extends any ? (FilterKeys<Obj, KeyPattern> extends never ? Default : FilterKeys<Obj, KeyPattern>) : never;
93+
9194
/** Return any `[string]/[string]` media type (important because openapi-fetch allows any content response, not just JSON-like) */
9295
export type MediaType = `${string}/${string}`;
9396
/** Return any media type containing "json" (works for "application/json", "application/vnd.api+json", "application/vnd.oai.openapi+json") */

0 commit comments

Comments
 (0)