Skip to content
This repository has been archived by the owner on May 22, 2023. It is now read-only.

Commit

Permalink
Fix TS lib error, simplify generated types (#24)
Browse files Browse the repository at this point in the history
  • Loading branch information
drwpow committed Apr 13, 2023
1 parent 70763c2 commit 8e7cb46
Show file tree
Hide file tree
Showing 4 changed files with 102 additions and 94 deletions.
5 changes: 5 additions & 0 deletions .changeset/light-lamps-chew.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'openapi-fetch': patch
---

Fix TypeScript lib error, simplify generated types
Empty file added schema.ts
Empty file.
190 changes: 96 additions & 94 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,22 @@ export interface BaseParams {
query?: Record<string, unknown>;
}

export const methods = ['get', 'put', 'post', 'delete', 'options', 'head', 'patch', 'trace'] as const;
export type Method = (typeof methods)[number];
export type Method = 'get' | 'put' | 'post' | 'delete' | 'options' | 'head' | 'patch' | 'trace';

/** Gets a union of paths which have method */
type PathsWith<T, M extends Method> = {
[Path in keyof T]: T[Path] extends { [K in M]: unknown } ? Path : never;
}[keyof T];

type PathParams<T> = T extends { parameters: any } ? { params: T['parameters'] } : { params?: BaseParams };
type MethodParams<T> = T extends {
parameters: any;
}
? { params: T['parameters'] }
: { params?: BaseParams };
type Params<T> = PathParams<T> & MethodParams<T>;
type RequestBody<T> = T extends { requestBody: any } ? { body: Unwrap<T['requestBody']> } : { body?: never };
type FetchOptions<T> = Params<T> & RequestBody<T> & Omit<RequestInit, 'body'>;

type TruncatedResponse = Omit<Response, 'arrayBuffer' | 'blob' | 'body' | 'clone' | 'formData' | 'json' | 'text'>;
/** Infer request/response from content type */
Expand All @@ -28,101 +42,89 @@ type Unwrap<T> = T extends {
? T['content']['*/*']
: T;

export default function createClient<T>(defaultOptions?: ClientOptions) {
type Success<T> = T extends { 200: any } ? T[200] : T extends { 201: any } ? T[201] : T extends { 202: any } ? T[202] : T extends { default: any } ? T['default'] : unknown;
type Error<T> = T extends { 500: any }
? T[500]
: T extends { 404: any }
? T[404]
: T extends { 402: any }
? T[402]
: T extends { 401: any }
? T[401]
: T extends { 400: any }
? T[400]
: T extends { 422: any }
? T[422]
: T extends { 418: any }
? T[418]
: T extends { 417: any }
? T[417]
: T extends { 416: any }
? T[416]
: T extends { 415: any }
? T[415]
: T extends { 414: any }
? T[414]
: T extends { 413: any }
? T[413]
: T extends { 412: any }
? T[412]
: T extends { 411: any }
? T[411]
: T extends { 410: any }
? T[410]
: T extends { 409: any }
? T[409]
: T extends { 408: any }
? T[408]
: T extends { 407: any }
? T[407]
: T extends { 406: any }
? T[406]
: T extends { 405: any }
? T[405]
: T extends { default: any }
? T['default']
: unknown;
type FetchResponse<T> =
| {
data: T extends { responses: any } ? NonNullable<Unwrap<Success<T['responses']>>> : unknown;
error?: never;
response: TruncatedResponse;
}
| {
data?: never;
error: T extends { responses: any } ? NonNullable<Unwrap<Error<T['responses']>>> : unknown;
response: TruncatedResponse;
};

export default function createClient<T>(options?: ClientOptions) {
const defaultHeaders = new Headers({
...DEFAULT_HEADERS,
...(defaultOptions?.headers ?? {}),
...(options?.headers ?? {}),
});

/** Gets a union of paths which have method */
type PathsWith<M extends Method> = {
[Path in keyof T]: T[Path] extends { [K in M]: unknown } ? Path : never;
}[keyof T];
async function coreFetch<U extends keyof T, M extends keyof T[U]>(url: U, fetchOptions: FetchOptions<T[U][M]>): Promise<FetchResponse<T[U][M]>> {
let { headers, body, params = {}, ...init } = fetchOptions || {};

type FetchResponse<T> =
| {
data: T extends { responses: any } ? NonNullable<Unwrap<Success<T['responses']>>> : unknown;
error?: never;
response: TruncatedResponse;
}
| {
data?: never;
error: T extends { responses: any } ? NonNullable<Unwrap<Error<T['responses']>>> : unknown;
response: TruncatedResponse;
};

type PathParams<T> = T extends { parameters: any } ? { params: T['parameters'] } : { params?: BaseParams };
type MethodParams<T> = T extends {
parameters: any;
}
? { params: T['parameters'] }
: { params?: BaseParams };
type Params<T> = PathParams<T> & MethodParams<T>;
type RequestBody<T> = T extends { requestBody: any } ? { body: Unwrap<T['requestBody']> } : { body?: never };
type FetchOptions<T> = Params<T> & RequestBody<T> & Omit<RequestInit, 'body'>;
type Success<T> = T extends { 200: any } ? T[200] : T extends { 201: any } ? T[201] : T extends { 202: any } ? T[202] : T extends { default: any } ? T['default'] : unknown;
type Error<T> = T extends { 500: any }
? T[500]
: T extends { 404: any }
? T[404]
: T extends { 402: any }
? T[402]
: T extends { 401: any }
? T[401]
: T extends { 400: any }
? T[400]
: T extends { 422: any }
? T[422]
: T extends { 418: any }
? T[418]
: T extends { 417: any }
? T[417]
: T extends { 416: any }
? T[416]
: T extends { 415: any }
? T[415]
: T extends { 414: any }
? T[414]
: T extends { 413: any }
? T[413]
: T extends { 412: any }
? T[412]
: T extends { 411: any }
? T[411]
: T extends { 410: any }
? T[410]
: T extends { 409: any }
? T[409]
: T extends { 408: any }
? T[408]
: T extends { 407: any }
? T[407]
: T extends { 406: any }
? T[406]
: T extends { 405: any }
? T[405]
: T extends { default: any }
? T['default']
: unknown;

async function coreFetch<U extends keyof T, M extends keyof T[U]>(url: U, options: FetchOptions<T[U][M]>): Promise<FetchResponse<T[U][M]>> {
let { headers, body, params = {}, ...init } = options || {};
// URL
let finalURL = `${defaultOptions?.baseUrl ?? ''}${url as string}`;
let finalURL = `${options?.baseUrl ?? ''}${url as string}`;
const { path, query } = (params as BaseParams | undefined) ?? {};
if (path) for (const [k, v] of Object.entries(path)) finalURL = finalURL.replace(`{${k}}`, encodeURIComponent(`${v}`.trim()));
if (query) finalURL = `${finalURL}?${new URLSearchParams(query as any).toString()}`;

// headers
const baseHeaders = new Headers(defaultHeaders); // clone defaults (don’t overwrite!)
const headerOverrides = new Headers(headers);
for (const [k, v] of headerOverrides.entries()) {
if (v === undefined || v === null) baseHeaders.delete(k); // allow `undefined` | `null` to erase value
else baseHeaders.set(k, v);
}

// fetch!
const res = await fetch(finalURL, {
redirect: 'follow',
...defaultOptions,
...options,
...init,
headers: baseHeaders,
body: typeof body === 'string' ? body : JSON.stringify(body),
Expand All @@ -142,36 +144,36 @@ export default function createClient<T>(defaultOptions?: ClientOptions) {

return {
/** Call a GET endpoint */
async get<U extends PathsWith<'get'>, M extends keyof T[U]>(url: U, options: FetchOptions<T[U][M]>) {
return coreFetch(url, { ...options, method: 'GET' });
async get<U extends PathsWith<T, 'get'>, M extends keyof T[U]>(url: U, init: FetchOptions<T[U][M]>) {
return coreFetch(url, { ...init, method: 'GET' });
},
/** Call a PUT endpoint */
async put<U extends PathsWith<'put'>, M extends keyof T[U]>(url: U, options: FetchOptions<T[U][M]>) {
return coreFetch(url, { ...options, method: 'PUT' });
async put<U extends PathsWith<T, 'put'>, M extends keyof T[U]>(url: U, init: FetchOptions<T[U][M]>) {
return coreFetch(url, { ...init, method: 'PUT' });
},
/** Call a POST endpoint */
async post<U extends PathsWith<'post'>, M extends keyof T[U]>(url: U, options: FetchOptions<T[U][M]>) {
return coreFetch(url, { ...options, method: 'POST' });
async post<U extends PathsWith<T, 'post'>, M extends keyof T[U]>(url: U, init: FetchOptions<T[U][M]>) {
return coreFetch(url, { ...init, method: 'POST' });
},
/** Call a DELETE endpoint */
async del<U extends PathsWith<'delete'>, M extends keyof T[U]>(url: U, options: FetchOptions<T[U][M]>) {
return coreFetch(url, { ...options, method: 'DELETE' });
async del<U extends PathsWith<T, 'delete'>, M extends keyof T[U]>(url: U, init: FetchOptions<T[U][M]>) {
return coreFetch(url, { ...init, method: 'DELETE' });
},
/** Call a OPTIONS endpoint */
async options<U extends PathsWith<'options'>, M extends keyof T[U]>(url: U, options: FetchOptions<T[U][M]>) {
return coreFetch(url, { ...options, method: 'OPTIONS' });
async options<U extends PathsWith<T, 'options'>, M extends keyof T[U]>(url: U, init: FetchOptions<T[U][M]>) {
return coreFetch(url, { ...init, method: 'OPTIONS' });
},
/** Call a HEAD endpoint */
async head<U extends PathsWith<'head'>, M extends keyof T[U]>(url: U, options: FetchOptions<T[U][M]>) {
return coreFetch(url, { ...options, method: 'HEAD' });
async head<U extends PathsWith<T, 'head'>, M extends keyof T[U]>(url: U, init: FetchOptions<T[U][M]>) {
return coreFetch(url, { ...init, method: 'HEAD' });
},
/** Call a PATCH endpoint */
async patch<U extends PathsWith<'patch'>, M extends keyof T[U]>(url: U, options: FetchOptions<T[U][M]>) {
return coreFetch(url, { ...options, method: 'PATCH' });
async patch<U extends PathsWith<T, 'patch'>, M extends keyof T[U]>(url: U, init: FetchOptions<T[U][M]>) {
return coreFetch(url, { ...init, method: 'PATCH' });
},
/** Call a TRACE endpoint */
async trace<U extends PathsWith<'trace'>, M extends keyof T[U]>(url: U, options: FetchOptions<T[U][M]>) {
return coreFetch(url, { ...options, method: 'TRACE' });
async trace<U extends PathsWith<T, 'trace'>, M extends keyof T[U]>(url: U, init: FetchOptions<T[U][M]>) {
return coreFetch(url, { ...init, method: 'TRACE' });
},
};
}
1 change: 1 addition & 0 deletions tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
"compilerOptions": {
"allowSyntheticDefaultImports": true,
"declaration": true,
"downlevelIteration": false,
"esModuleInterop": true,
"module": "ESNext",
"moduleResolution": "nodenext",
Expand Down

0 comments on commit 8e7cb46

Please sign in to comment.