Skip to content

Commit

Permalink
Update openapi-fetch types
Browse files Browse the repository at this point in the history
  • Loading branch information
drwpow committed Oct 5, 2023
1 parent 487ef9b commit 04a024a
Show file tree
Hide file tree
Showing 6 changed files with 88 additions and 28 deletions.
5 changes: 5 additions & 0 deletions .changeset/beige-ligers-tell.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"openapi-fetch": patch
---

Fix GET requests requiring 2nd param when it’s not needed
5 changes: 5 additions & 0 deletions .changeset/rich-poems-swim.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"openapi-typescript": minor
---

**Feature**: Added debugger that lets you profile performance and see more in-depth messages
2 changes: 0 additions & 2 deletions docs/src/content/docs/openapi-fetch/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,8 +69,6 @@ Next, generate TypeScript types from your OpenAPI schema using openapi-typescrip
npx openapi-typescript ./path/to/api/v1.yaml -o ./src/lib/api/v1.d.ts
```

> ⚠️ Be sure to <a href="https://redocly.com/docs/cli/commands/lint/" target="_blank" rel="noopener noreferrer">validate your schemas</a>! openapi-typescript will err on invalid schemas.
Lastly, be sure to **run typechecking** in your project. This can be done by adding `tsc --noEmit` to your <a href="https://docs.npmjs.com/cli/v9/using-npm/scripts" target="_blank" rel="noopener noreferrer">npm scripts</a> like so:

```json
Expand Down
30 changes: 23 additions & 7 deletions packages/openapi-fetch/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,28 +32,46 @@ interface ClientOptions extends Omit<RequestInit, "headers"> {
// headers override to make typing friendlier
headers?: HeadersOptions;
}

export type HeadersOptions =
| HeadersInit
| Record<string, string | number | boolean | null | undefined>;

export type QuerySerializer<T> = (
query: T extends { parameters: any }
? NonNullable<T["parameters"]["query"]>
: Record<string, unknown>,
) => string;

export type BodySerializer<T> = (body: OperationRequestBodyContent<T>) => any;

export type ParseAs = "json" | "text" | "blob" | "arrayBuffer" | "stream";

export interface DefaultParamsOption {
params?: { query?: Record<string, unknown> };
}

export interface EmptyParameters {
query?: never;
header?: never;
path?: never;
cookie?: never;
}

export type ParamsOption<T> = T extends { parameters: any }
? { params: NonNullable<T["parameters"]> }
? T["parameters"] extends EmptyParameters
? DefaultParamsOption
: { params: NonNullable<T["parameters"]> }
: DefaultParamsOption;

export type RequestBodyOption<T> = OperationRequestBodyContent<T> extends never
? { body?: never }
: undefined extends OperationRequestBodyContent<T>
? { body?: OperationRequestBodyContent<T> }
: { body: OperationRequestBodyContent<T> };

export type FetchOptions<T> = RequestOptions<T> & Omit<RequestInit, "body">;

export type FetchResponse<T> =
| {
data: FilterKeys<SuccessResponse<ResponseObjectMap<T>>, MediaType>;
Expand All @@ -65,6 +83,7 @@ export type FetchResponse<T> =
error: FilterKeys<ErrorResponse<ResponseObjectMap<T>>, MediaType>;
response: Response;
};

export type RequestOptions<T> = ParamsOption<T> &
RequestBodyOption<T> & {
querySerializer?: QuerySerializer<T>;
Expand All @@ -81,7 +100,6 @@ export default function createClient<Paths extends {}>(
bodySerializer: globalBodySerializer,
...options
} = clientOptions;

let baseUrl = options.baseUrl ?? "";
if (baseUrl.endsWith("/")) {
baseUrl = baseUrl.slice(0, -1); // remove trailing slash
Expand Down Expand Up @@ -318,11 +336,9 @@ export function createFinalURL<O>(
finalURL = finalURL.replace(`{${k}}`, encodeURIComponent(String(v)));
}
}
if (options.params.query) {
const search = options.querySerializer(options.params.query as any);
if (search) {
finalURL += `?${search}`;
}
const search = options.querySerializer((options.params.query as any) ?? {});
if (search) {
finalURL += `?${search}`;
}
return finalURL;
}
Expand Down
60 changes: 48 additions & 12 deletions packages/openapi-typescript-helpers/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,15 @@

// HTTP types

export type HttpMethod = "get" | "put" | "post" | "delete" | "options" | "head" | "patch" | "trace";
export type HttpMethod =
| "get"
| "put"
| "post"
| "delete"
| "options"
| "head"
| "patch"
| "trace";
/** 2XX statuses */
export type OkStatus = 200 | 201 | 202 | 203 | 204 | 206 | 207 | "2XX";
// prettier-ignore
Expand All @@ -12,8 +20,15 @@ export type ErrorStatus = 500 | 501 | 502 | 503 | 504 | 505 | 506 | 507 | 508 |
// OpenAPI type helpers

/** Given an OpenAPI **Paths Object**, find all paths that have the given method */
export type PathsWithMethod<Paths extends Record<string, PathItemObject>, PathnameMethod extends HttpMethod> = {
[Pathname in keyof Paths]: Paths[Pathname] extends { [K in PathnameMethod]: any } ? Pathname : never;
export type PathsWithMethod<
Paths extends Record<string, PathItemObject>,
PathnameMethod extends HttpMethod,
> = {
[Pathname in keyof Paths]: Paths[Pathname] extends {
[K in PathnameMethod]: any;
}
? Pathname
: never;
}[keyof Paths];
/** DO NOT USE! Only used only for OperationObject type inference */
export interface OperationObject {
Expand All @@ -23,28 +38,49 @@ export interface OperationObject {
responses: any;
}
/** Internal helper used in PathsWithMethod */
export type PathItemObject = { [M in HttpMethod]: OperationObject } & { parameters?: any };
export type PathItemObject = {
[M in HttpMethod]: OperationObject;
} & { parameters?: any };
/** Return `responses` for an Operation Object */
export type ResponseObjectMap<T> = T extends { responses: any } ? T["responses"] : unknown;
export type ResponseObjectMap<T> = T extends { responses: any }
? T["responses"]
: unknown;
/** Return `content` for a Response Object */
export type ResponseContent<T> = T extends { content: any } ? T["content"] : unknown;
export type ResponseContent<T> = T extends { content: any }
? T["content"]
: unknown;
/** Return `requestBody` for an Operation Object */
export type OperationRequestBody<T> = T extends { requestBody?: any } ? T["requestBody"] : never;
export type OperationRequestBody<T> = T extends { requestBody?: any }
? T["requestBody"]
: never;
/** Internal helper used in OperationRequestBodyContent */
export type OperationRequestBodyMediaContent<T> = undefined extends OperationRequestBody<T> ? FilterKeys<NonNullable<OperationRequestBody<T>>, "content"> | undefined : FilterKeys<OperationRequestBody<T>, "content">;
export type OperationRequestBodyMediaContent<T> =
undefined extends OperationRequestBody<T>
? FilterKeys<NonNullable<OperationRequestBody<T>>, "content"> | undefined
: FilterKeys<OperationRequestBody<T>, "content">;
/** Return first `content` from a Request Object Mapping, allowing any media type */
export type OperationRequestBodyContent<T> = FilterKeys<OperationRequestBodyMediaContent<T>, MediaType> extends never
? FilterKeys<NonNullable<OperationRequestBodyMediaContent<T>>, MediaType> | undefined
export type OperationRequestBodyContent<T> = FilterKeys<
OperationRequestBodyMediaContent<T>,
MediaType
> extends never
?
| FilterKeys<NonNullable<OperationRequestBodyMediaContent<T>>, MediaType>
| undefined
: FilterKeys<OperationRequestBodyMediaContent<T>, MediaType>;
/** Return first 2XX response from a Response Object Map */
export type SuccessResponse<T> = FilterKeys<FilterKeys<T, OkStatus>, "content">;
/** Return first 5XX or 4XX response (in that order) from a Response Object Map */
export type ErrorResponse<T> = FilterKeys<FilterKeys<T, ErrorStatus>, "content">;
export type ErrorResponse<T> = FilterKeys<
FilterKeys<T, ErrorStatus>,
"content"
>;

// Generic TS utils

/** Find first match of multiple keys */
export type FilterKeys<Obj, Matchers> = { [K in keyof Obj]: K extends Matchers ? Obj[K] : never }[keyof Obj];
export type FilterKeys<Obj, Matchers> = {
[K in keyof Obj]: K extends Matchers ? Obj[K] : never;
}[keyof Obj];
/** Return any `[string]/[string]` media type (important because openapi-fetch allows any content response, not just JSON-like) */
export type MediaType = `${string}/${string}`;
/** Filter objects that have required keys */
Expand Down
14 changes: 7 additions & 7 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit 04a024a

Please sign in to comment.