@@ -183,91 +183,68 @@ describe("multiple success responses", () => {
183
183
184
184
export type Fetcher = (method: Method, url: string, parameters?: EndpointParameters | undefined) => Promise<Response>;
185
185
186
- // Status code type for success responses
187
- export type SuccessStatusCode =
188
- | 200
189
- | 201
190
- | 202
191
- | 203
192
- | 204
193
- | 205
194
- | 206
195
- | 207
196
- | 208
197
- | 226
198
- | 300
199
- | 301
200
- | 302
201
- | 303
202
- | 304
203
- | 305
204
- | 306
205
- | 307
206
- | 308;
186
+ export const successStatusCodes = [
187
+ 200, 201, 202, 203, 204, 205, 206, 207, 208, 226, 300, 301, 302, 303, 304, 305, 306, 307, 308,
188
+ ] as const;
189
+ export type SuccessStatusCode = (typeof successStatusCodes)[number];
190
+
191
+ export const errorStatusCodes = [
192
+ 400, 401, 402, 403, 404, 405, 406, 407, 408, 409, 410, 411, 412, 413, 414, 415, 416, 417, 418, 421, 422, 423, 424,
193
+ 425, 426, 428, 429, 431, 451, 500, 501, 502, 503, 504, 505, 506, 507, 508, 510, 511,
194
+ ] as const;
195
+ export type ErrorStatusCode = (typeof errorStatusCodes)[number];
207
196
208
197
// Error handling types
198
+ /** @see https://developer.mozilla.org/en-US/docs/Web/API/Response */
199
+ interface SuccessResponse<TSuccess, TStatusCode> extends Omit<Response, "ok" | "status" | "json"> {
200
+ ok: true;
201
+ status: TStatusCode;
202
+ data: TSuccess;
203
+ /** [MDN Reference](https://developer.mozilla.org/en-US/docs/Web/API/Response/json) */
204
+ json: () => Promise<TSuccess>;
205
+ }
206
+
207
+ /** @see https://developer.mozilla.org/en-US/docs/Web/API/Response */
208
+ interface ErrorResponse<TData, TStatusCode> extends Omit<Response, "ok" | "status" | "json"> {
209
+ ok: false;
210
+ status: TStatusCode;
211
+ data: TData;
212
+ /** [MDN Reference](https://developer.mozilla.org/en-US/docs/Web/API/Response/json) */
213
+ json: () => Promise<TData>;
214
+ }
215
+
209
216
export type TypedApiResponse<
210
217
TSuccess,
211
218
TAllResponses extends Record<string | number, unknown> = {},
212
219
> = keyof TAllResponses extends never
213
- ? Omit<Response, "ok" | "status" | "json"> & {
214
- ok: true;
215
- status: number;
216
- data: TSuccess;
217
- json: () => Promise<TSuccess>;
218
- }
220
+ ? SuccessResponse<TSuccess, number>
219
221
: {
220
222
[K in keyof TAllResponses]: K extends string
221
223
? K extends \`\${infer TStatusCode extends number}\`
222
224
? TStatusCode extends SuccessStatusCode
223
- ? Omit<Response, "ok" | "status" | "json"> & {
224
- ok: true;
225
- status: TStatusCode;
226
- data: TSuccess;
227
- json: () => Promise<TSuccess>;
228
- }
229
- : Omit<Response, "ok" | "status" | "json"> & {
230
- ok: false;
231
- status: TStatusCode;
232
- data: TAllResponses[K];
233
- json: () => Promise<TAllResponses[K]>;
234
- }
225
+ ? SuccessResponse<TSuccess, TStatusCode>
226
+ : ErrorResponse<TAllResponses[K], TStatusCode>
235
227
: never
236
228
: K extends number
237
229
? K extends SuccessStatusCode
238
- ? Omit<Response, "ok" | "status" | "json"> & {
239
- ok: true;
240
- status: K;
241
- data: TSuccess;
242
- json: () => Promise<TSuccess>;
243
- }
244
- : Omit<Response, "ok" | "status" | "json"> & {
245
- ok: false;
246
- status: K;
247
- data: TAllResponses[K];
248
- json: () => Promise<TAllResponses[K]>;
249
- }
230
+ ? SuccessResponse<TSuccess, K>
231
+ : ErrorResponse<TAllResponses[K], K>
250
232
: never;
251
233
}[keyof TAllResponses];
252
234
253
235
export type SafeApiResponse<TEndpoint> = TEndpoint extends { response: infer TSuccess; responses: infer TResponses }
254
236
? TResponses extends Record<string, unknown>
255
237
? TypedApiResponse<TSuccess, TResponses>
256
- : Omit<Response, "ok" | "status" | "json"> & {
257
- ok: true;
258
- status: number;
259
- data: TSuccess;
260
- json: () => Promise<TSuccess>;
261
- }
238
+ : SuccessResponse<TSuccess, number>
262
239
: TEndpoint extends { response: infer TSuccess }
263
- ? Omit<Response, "ok" | "status" | "json"> & {
264
- ok: true;
265
- status: number;
266
- data: TSuccess;
267
- json: () => Promise<TSuccess>;
268
- }
240
+ ? SuccessResponse<TSuccess, number>
269
241
: never;
270
242
243
+ export type InferResponseByStatus<TEndpoint, TStatusCode> = Extract<
244
+ SafeApiResponse<TEndpoint>,
245
+ { status: TStatusCode }
246
+ >;
247
+
271
248
type RequiredKeys<T> = {
272
249
[P in keyof T]-?: undefined extends T[P] ? never : P;
273
250
}[keyof T];
@@ -276,9 +253,23 @@ describe("multiple success responses", () => {
276
253
277
254
// </ApiClientTypes>
278
255
256
+ // <TypedResponseError>
257
+ export class TypedResponseError extends Error {
258
+ response: ErrorResponse<unknown, ErrorStatusCode>;
259
+ status: number;
260
+ constructor(response: ErrorResponse<unknown, ErrorStatusCode>) {
261
+ super(\`HTTP \${response.status}: \${response.statusText}\`);
262
+ this.name = "TypedResponseError";
263
+ this.response = response;
264
+ this.status = response.status;
265
+ }
266
+ }
267
+ // </TypedResponseError>
279
268
// <ApiClient>
280
269
export class ApiClient {
281
270
baseUrl: string = "";
271
+ successStatusCodes = successStatusCodes;
272
+ errorStatusCodes = errorStatusCodes;
282
273
283
274
constructor(public fetcher: Fetcher) {}
284
275
@@ -298,12 +289,12 @@ describe("multiple success responses", () => {
298
289
// <ApiClient.post>
299
290
post<Path extends keyof PostEndpoints, TEndpoint extends PostEndpoints[Path]>(
300
291
path: Path,
301
- ...params: MaybeOptionalArg<TEndpoint["parameters"] & { withResponse?: false }>
292
+ ...params: MaybeOptionalArg<TEndpoint["parameters"] & { withResponse?: false; throwOnStatusError?: boolean }>
302
293
): Promise<TEndpoint["response"]>;
303
294
304
295
post<Path extends keyof PostEndpoints, TEndpoint extends PostEndpoints[Path]>(
305
296
path: Path,
306
- ...params: MaybeOptionalArg<TEndpoint["parameters"] & { withResponse: true }>
297
+ ...params: MaybeOptionalArg<TEndpoint["parameters"] & { withResponse: true; throwOnStatusError?: boolean }>
307
298
): Promise<SafeApiResponse<TEndpoint>>;
308
299
309
300
post<Path extends keyof PostEndpoints, TEndpoint extends PostEndpoints[Path]>(
@@ -312,31 +303,27 @@ describe("multiple success responses", () => {
312
303
): Promise<any> {
313
304
const requestParams = params[0];
314
305
const withResponse = requestParams?.withResponse;
306
+ const { withResponse: _, throwOnStatusError = withResponse ? false : true, ...fetchParams } = requestParams || {};
307
+
308
+ const promise = this.fetcher(
309
+ "post",
310
+ this.baseUrl + path,
311
+ Object.keys(fetchParams).length ? requestParams : undefined,
312
+ ).then(async (response) => {
313
+ const data = await this.parseResponse(response);
314
+ const typedResponse = Object.assign(response, {
315
+ data: data,
316
+ json: () => Promise.resolve(data),
317
+ }) as SafeApiResponse<TEndpoint>;
318
+
319
+ if (throwOnStatusError && errorStatusCodes.includes(response.status as never)) {
320
+ throw new TypedResponseError(typedResponse as never);
321
+ }
315
322
316
- // Remove withResponse from params before passing to fetcher
317
- const { withResponse: _, ...fetchParams } = requestParams || {};
318
-
319
- if (withResponse) {
320
- return this.fetcher("post", this.baseUrl + path, Object.keys(fetchParams).length ? fetchParams : undefined).then(
321
- async (response) => {
322
- // Parse the response data
323
- const data = await this.parseResponse(response);
324
-
325
- // Override properties while keeping the original Response object
326
- const typedResponse = Object.assign(response, {
327
- ok: response.ok,
328
- status: response.status,
329
- data: data,
330
- json: () => Promise.resolve(data),
331
- });
332
- return typedResponse;
333
- },
334
- );
335
- } else {
336
- return this.fetcher("post", this.baseUrl + path, requestParams).then((response) =>
337
- this.parseResponse(response),
338
- ) as Promise<TEndpoint["response"]>;
339
- }
323
+ return withResponse ? typedResponse : data;
324
+ });
325
+
326
+ return promise as Promise<TEndpoint["response"]>;
340
327
}
341
328
// </ApiClient.post>
342
329
@@ -352,13 +339,10 @@ describe("multiple success responses", () => {
352
339
method: TMethod,
353
340
path: TPath,
354
341
...params: MaybeOptionalArg<TEndpoint extends { parameters: infer Params } ? Params : never>
355
- ): Promise<
356
- Omit<Response, "json"> & {
357
- /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Request/json) */
358
- json: () => Promise<TEndpoint extends { response: infer Res } ? Res : never>;
359
- }
360
- > {
361
- return this.fetcher(method, this.baseUrl + (path as string), params[0] as EndpointParameters);
342
+ ): Promise<SafeApiResponse<TEndpoint>> {
343
+ return this.fetcher(method, this.baseUrl + (path as string), params[0] as EndpointParameters) as Promise<
344
+ SafeApiResponse<TEndpoint>
345
+ >;
362
346
}
363
347
// </ApiClient.request>
364
348
}
@@ -392,7 +376,7 @@ describe("multiple success responses", () => {
392
376
}
393
377
*/
394
378
395
- // </ApiClient
379
+ // </ApiClient>
396
380
"
397
381
` ) ;
398
382
} ) ;
0 commit comments