Skip to content

Commit 30ca09b

Browse files
committed
Update openapi-fetch types
1 parent cc8817e commit 30ca09b

File tree

4 files changed

+89
-21
lines changed

4 files changed

+89
-21
lines changed

.changeset/beige-ligers-tell.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"openapi-fetch": patch
3+
---
4+
5+
Fix GET requests requiring 2nd param when it’s not needed

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

+2-2
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ describe("client", () => {
5151
status: 200,
5252
body: JSON.stringify(["one", "two", "three"]),
5353
});
54-
const dataRes = await client.GET("/string-array", {});
54+
const dataRes = await client.GET("/string-array");
5555

5656
// … is initially possibly undefined
5757
// @ts-expect-error
@@ -73,7 +73,7 @@ describe("client", () => {
7373
status: 500,
7474
body: JSON.stringify({ code: 500, message: "Something went wrong" }),
7575
});
76-
const errorRes = await client.GET("/string-array", {});
76+
const errorRes = await client.GET("/string-array");
7777

7878
// … is initially possibly undefined
7979
// @ts-expect-error

packages/openapi-fetch/src/index.ts

+34-7
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ import type {
1313
const DEFAULT_HEADERS = {
1414
"Content-Type": "application/json",
1515
};
16-
const TRAILING_SLASH_RE = /\/*$/;
1716

1817
// Note: though "any" is considered bad practice in general, this library relies
1918
// on "any" for type inference only it can give. Same goes for the "{}" type.
@@ -32,28 +31,46 @@ interface ClientOptions extends Omit<RequestInit, "headers"> {
3231
// headers override to make typing friendlier
3332
headers?: HeadersOptions;
3433
}
34+
3535
export type HeadersOptions =
3636
| HeadersInit
3737
| Record<string, string | number | boolean | null | undefined>;
38+
3839
export type QuerySerializer<T> = (
3940
query: T extends { parameters: any }
4041
? NonNullable<T["parameters"]["query"]>
4142
: Record<string, unknown>,
4243
) => string;
44+
4345
export type BodySerializer<T> = (body: OperationRequestBodyContent<T>) => any;
46+
4447
export type ParseAs = "json" | "text" | "blob" | "arrayBuffer" | "stream";
48+
4549
export interface DefaultParamsOption {
4650
params?: { query?: Record<string, unknown> };
4751
}
52+
53+
export interface EmptyParameters {
54+
query?: never;
55+
header?: never;
56+
path?: never;
57+
cookie?: never;
58+
}
59+
4860
export type ParamsOption<T> = T extends { parameters: any }
49-
? { params: NonNullable<T["parameters"]> }
61+
? T["parameters"] extends EmptyParameters
62+
? DefaultParamsOption
63+
: { params: NonNullable<T["parameters"]> }
5064
: DefaultParamsOption;
65+
5166
export type RequestBodyOption<T> = OperationRequestBodyContent<T> extends never
5267
? { body?: never }
5368
: undefined extends OperationRequestBodyContent<T>
5469
? { body?: OperationRequestBodyContent<T> }
5570
: { body: OperationRequestBodyContent<T> };
71+
5672
export type FetchOptions<T> = RequestOptions<T> & Omit<RequestInit, "body">;
73+
5774
export type FetchResponse<T> =
5875
| {
5976
data: FilterKeys<SuccessResponse<ResponseObjectMap<T>>, MediaType>;
@@ -65,6 +82,7 @@ export type FetchResponse<T> =
6582
error: FilterKeys<ErrorResponse<ResponseObjectMap<T>>, MediaType>;
6683
response: Response;
6784
};
85+
6886
export type RequestOptions<T> = ParamsOption<T> &
6987
RequestBodyOption<T> & {
7088
querySerializer?: QuerySerializer<T>;
@@ -159,11 +177,18 @@ export default function createClient<Paths extends {}>(
159177
return { error, response: response as any };
160178
}
161179

180+
type GetPaths = PathsWithMethod<Paths, "get">;
181+
type GetFetchOptions<P extends GetPaths> = FetchOptions<
182+
FilterKeys<Paths[P], "get">
183+
>;
184+
162185
return {
163186
/** Call a GET endpoint */
164-
async GET<P extends PathsWithMethod<Paths, "get">>(
187+
async GET<P extends GetPaths>(
165188
url: P,
166-
init: FetchOptions<FilterKeys<Paths[P], "get">>,
189+
...init: GetFetchOptions<P> extends DefaultParamsOption
190+
? [GetFetchOptions<P>?]
191+
: [GetFetchOptions<P>]
167192
) {
168193
return coreFetch<P, "get">(url, { ...init, method: "GET" } as any);
169194
},
@@ -252,9 +277,11 @@ export function createFinalURL<O>(
252277
querySerializer: QuerySerializer<O>;
253278
},
254279
): string {
255-
let finalURL = `${
256-
options.baseUrl ? options.baseUrl.replace(TRAILING_SLASH_RE, "") : ""
257-
}${url as string}`;
280+
let finalURL = options.baseUrl ?? "";
281+
if (finalURL.endsWith("/")) {
282+
finalURL = finalURL.slice(0, -1);
283+
}
284+
finalURL += url;
258285
if (options.params.path) {
259286
for (const [k, v] of Object.entries(options.params.path)) {
260287
finalURL = finalURL.replace(`{${k}}`, encodeURIComponent(String(v)));

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

+48-12
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,15 @@
22

33
// HTTP types
44

5-
export type HttpMethod = "get" | "put" | "post" | "delete" | "options" | "head" | "patch" | "trace";
5+
export type HttpMethod =
6+
| "get"
7+
| "put"
8+
| "post"
9+
| "delete"
10+
| "options"
11+
| "head"
12+
| "patch"
13+
| "trace";
614
/** 2XX statuses */
715
export type OkStatus = 200 | 201 | 202 | 203 | 204 | 206 | 207 | "2XX";
816
// prettier-ignore
@@ -12,8 +20,15 @@ export type ErrorStatus = 500 | 501 | 502 | 503 | 504 | 505 | 506 | 507 | 508 |
1220
// OpenAPI type helpers
1321

1422
/** Given an OpenAPI **Paths Object**, find all paths that have the given method */
15-
export type PathsWithMethod<Paths extends Record<string, PathItemObject>, PathnameMethod extends HttpMethod> = {
16-
[Pathname in keyof Paths]: Paths[Pathname] extends { [K in PathnameMethod]: any } ? Pathname : never;
23+
export type PathsWithMethod<
24+
Paths extends Record<string, PathItemObject>,
25+
PathnameMethod extends HttpMethod,
26+
> = {
27+
[Pathname in keyof Paths]: Paths[Pathname] extends {
28+
[K in PathnameMethod]: any;
29+
}
30+
? Pathname
31+
: never;
1732
}[keyof Paths];
1833
/** DO NOT USE! Only used only for OperationObject type inference */
1934
export interface OperationObject {
@@ -23,27 +38,48 @@ export interface OperationObject {
2338
responses: any;
2439
}
2540
/** Internal helper used in PathsWithMethod */
26-
export type PathItemObject = { [M in HttpMethod]: OperationObject } & { parameters?: any };
41+
export type PathItemObject = {
42+
[M in HttpMethod]: OperationObject;
43+
} & { parameters?: any };
2744
/** Return `responses` for an Operation Object */
28-
export type ResponseObjectMap<T> = T extends { responses: any } ? T["responses"] : unknown;
45+
export type ResponseObjectMap<T> = T extends { responses: any }
46+
? T["responses"]
47+
: unknown;
2948
/** Return `content` for a Response Object */
30-
export type ResponseContent<T> = T extends { content: any } ? T["content"] : unknown;
49+
export type ResponseContent<T> = T extends { content: any }
50+
? T["content"]
51+
: unknown;
3152
/** Return `requestBody` for an Operation Object */
32-
export type OperationRequestBody<T> = T extends { requestBody?: any } ? T["requestBody"] : never;
53+
export type OperationRequestBody<T> = T extends { requestBody?: any }
54+
? T["requestBody"]
55+
: never;
3356
/** Internal helper used in OperationRequestBodyContent */
34-
export type OperationRequestBodyMediaContent<T> = undefined extends OperationRequestBody<T> ? FilterKeys<NonNullable<OperationRequestBody<T>>, "content"> | undefined : FilterKeys<OperationRequestBody<T>, "content">;
57+
export type OperationRequestBodyMediaContent<T> =
58+
undefined extends OperationRequestBody<T>
59+
? FilterKeys<NonNullable<OperationRequestBody<T>>, "content"> | undefined
60+
: FilterKeys<OperationRequestBody<T>, "content">;
3561
/** Return first `content` from a Request Object Mapping, allowing any media type */
36-
export type OperationRequestBodyContent<T> = FilterKeys<OperationRequestBodyMediaContent<T>, MediaType> extends never
37-
? FilterKeys<NonNullable<OperationRequestBodyMediaContent<T>>, MediaType> | undefined
62+
export type OperationRequestBodyContent<T> = FilterKeys<
63+
OperationRequestBodyMediaContent<T>,
64+
MediaType
65+
> extends never
66+
?
67+
| FilterKeys<NonNullable<OperationRequestBodyMediaContent<T>>, MediaType>
68+
| undefined
3869
: FilterKeys<OperationRequestBodyMediaContent<T>, MediaType>;
3970
/** Return first 2XX response from a Response Object Map */
4071
export type SuccessResponse<T> = FilterKeys<FilterKeys<T, OkStatus>, "content">;
4172
/** Return first 5XX or 4XX response (in that order) from a Response Object Map */
42-
export type ErrorResponse<T> = FilterKeys<FilterKeys<T, ErrorStatus>, "content">;
73+
export type ErrorResponse<T> = FilterKeys<
74+
FilterKeys<T, ErrorStatus>,
75+
"content"
76+
>;
4377

4478
// Generic TS utils
4579

4680
/** Find first match of multiple keys */
47-
export type FilterKeys<Obj, Matchers> = { [K in keyof Obj]: K extends Matchers ? Obj[K] : never }[keyof Obj];
81+
export type FilterKeys<Obj, Matchers> = {
82+
[K in keyof Obj]: K extends Matchers ? Obj[K] : never;
83+
}[keyof Obj];
4884
/** Return any `[string]/[string]` media type (important because openapi-fetch allows any content response, not just JSON-like) */
4985
export type MediaType = `${string}/${string}`;

0 commit comments

Comments
 (0)