Skip to content

Commit be590a4

Browse files
committed
feat: return Response object directly when using withResponse
Improves type-safe API error handling and response access Refactors API client response typing to unify success and error data under a consistent interface. Replaces separate error property with direct data access and ensures the Response object retains its methods. Updates documentation with clearer examples for type-safe error handling and data access patterns. Facilitates more ergonomic and predictable client usage, especially for error cases.
1 parent 86bdb4c commit be590a4

File tree

3 files changed

+99
-28
lines changed

3 files changed

+99
-28
lines changed

README.md

Lines changed: 17 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -93,27 +93,34 @@ const user = await api.get("/users/{id}", {
9393
path: { id: "123" }
9494
}); // user is directly typed as User object
9595

96-
// WithResponse: Full response details (for error handling)
96+
// WithResponse: Full Response object with typed ok/status and data
9797
const result = await api.get("/users/{id}", {
9898
path: { id: "123" },
9999
withResponse: true
100100
});
101101

102+
// result is the actual Response object with typed ok/status overrides plus data access
102103
if (result.ok) {
103-
// result.data is typed as the success response
104-
console.log("User:", result.data.name);
105-
// result.status is typed as success status codes (200, 201, etc.)
104+
// Access data directly (already parsed)
105+
const user = result.data; // Type: User
106+
console.log("User:", user.name);
107+
108+
// Or use json() method for compatibility
109+
const userFromJson = await result.json(); // Same as result.data
110+
console.log("User from json():", userFromJson.name);
111+
112+
console.log("Status:", result.status); // Typed as success status codes
113+
console.log("Headers:", result.headers); // Access to all Response properties
106114
} else {
107-
// result.error is typed based on documented error responses
115+
// Access error data directly
116+
const error = result.data; // Type based on status code
108117
if (result.status === 404) {
109-
console.log("User not found:", result.error.message);
118+
console.log("User not found:", error.message);
110119
} else if (result.status === 401) {
111-
console.log("Unauthorized:", result.error.details);
120+
console.log("Unauthorized:", error.details);
112121
}
113122
}
114-
```
115-
116-
### Success Response Type-Narrowing
123+
```### Success Response Type-Narrowing
117124
118125
When endpoints have multiple success responses (200, 201, etc.), the type is automatically narrowed based on status:
119126

packages/typed-openapi/API_CLIENT_EXAMPLES.md

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,3 +151,40 @@ if (!response.ok) {
151151
throw new ApiError(response.status, response.statusText, response);
152152
}
153153
```
154+
155+
## Error Handling with withResponse
156+
157+
For type-safe error handling without exceptions, use the `withResponse: true` option:
158+
159+
```typescript
160+
// Example with both data access methods
161+
const result = await api.get("/users/{id}", {
162+
path: { id: "123" },
163+
withResponse: true
164+
});
165+
166+
if (result.ok) {
167+
// Access data directly (already parsed)
168+
const user = result.data; // Type: User
169+
console.log("User:", user.name);
170+
171+
// Or use json() method for compatibility
172+
const userFromJson = await result.json(); // Same as result.data
173+
console.log("Same user:", userFromJson.name);
174+
175+
// Access other Response properties
176+
console.log("Status:", result.status);
177+
console.log("Headers:", result.headers.get("content-type"));
178+
} else {
179+
// Handle errors with proper typing
180+
const error = result.data; // Type based on status code
181+
182+
if (result.status === 404) {
183+
console.error("Not found:", error.message);
184+
} else if (result.status === 401) {
185+
console.error("Unauthorized:", error.details);
186+
} else {
187+
console.error("Unknown error:", error);
188+
}
189+
}
190+
```

packages/typed-openapi/src/generator.ts

Lines changed: 45 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -343,47 +343,62 @@ export type StatusCode = ${statusCodeType};
343343
// Error handling types
344344
export type TypedApiResponse<TSuccess, TAllResponses extends Record<string | number, unknown> = {}> =
345345
(keyof TAllResponses extends never
346-
? {
346+
? Omit<Response, "ok" | "status" | "json"> & {
347347
ok: true;
348348
status: number;
349349
data: TSuccess;
350+
json: () => Promise<TSuccess>;
350351
}
351352
: {
352353
[K in keyof TAllResponses]: K extends string
353354
? K extends \`\${infer TStatusCode extends number}\`
354355
? TStatusCode extends StatusCode
355-
? {
356+
? Omit<Response, "ok" | "status" | "json"> & {
356357
ok: true;
357358
status: TStatusCode;
358-
data: TAllResponses[K];
359+
data: TSuccess;
360+
json: () => Promise<TSuccess>;
359361
}
360-
: {
362+
: Omit<Response, "ok" | "status" | "json"> & {
361363
ok: false;
362364
status: TStatusCode;
363-
error: TAllResponses[K];
365+
data: TAllResponses[K];
366+
json: () => Promise<TAllResponses[K]>;
364367
}
365368
: never
366369
: K extends number
367370
? K extends StatusCode
368-
? {
371+
? Omit<Response, "ok" | "status" | "json"> & {
369372
ok: true;
370373
status: K;
371-
data: TAllResponses[K];
374+
data: TSuccess;
375+
json: () => Promise<TSuccess>;
372376
}
373-
: {
377+
: Omit<Response, "ok" | "status" | "json"> & {
374378
ok: false;
375379
status: K;
376-
error: TAllResponses[K];
380+
data: TAllResponses[K];
381+
json: () => Promise<TAllResponses[K]>;
377382
}
378383
: never;
379384
}[keyof TAllResponses]);
380385
381386
export type SafeApiResponse<TEndpoint> = TEndpoint extends { response: infer TSuccess; responses: infer TResponses }
382387
? TResponses extends Record<string, unknown>
383388
? TypedApiResponse<TSuccess, TResponses>
384-
: { ok: true; status: number; data: TSuccess }
389+
: Omit<Response, "ok" | "status" | "json"> & {
390+
ok: true;
391+
status: number;
392+
data: TSuccess;
393+
json: () => Promise<TSuccess>;
394+
}
385395
: TEndpoint extends { response: infer TSuccess }
386-
? { ok: true; status: number; data: TSuccess }
396+
? Omit<Response, "ok" | "status" | "json"> & {
397+
ok: true;
398+
status: number;
399+
data: TSuccess;
400+
json: () => Promise<TSuccess>;
401+
}
387402
: never;
388403
389404
type RequiredKeys<T> = {
@@ -454,12 +469,17 @@ export class ApiClient {
454469
if (withResponse) {
455470
return this.fetcher("${method}", this.baseUrl + path, Object.keys(fetchParams).length ? fetchParams : undefined)
456471
.then(async (response) => {
472+
// Parse the response data
457473
const data = await this.parseResponse(response);
458-
if (response.ok) {
459-
return { ok: true, status: response.status, data };
460-
} else {
461-
return { ok: false, status: response.status, error: data };
462-
}
474+
475+
// Override properties while keeping the original Response object
476+
const typedResponse = Object.assign(response, {
477+
ok: response.ok,
478+
status: response.status,
479+
data: data,
480+
json: () => Promise.resolve(data)
481+
});
482+
return typedResponse;
463483
});
464484
} else {
465485
return this.fetcher("${method}", this.baseUrl + path, requestParams)
@@ -530,9 +550,16 @@ export function createApiClient(fetcher: Fetcher, baseUrl?: string) {
530550
// With error handling
531551
const result = await api.get("/users/{id}", { path: { id: "123" }, withResponse: true });
532552
if (result.ok) {
533-
console.log(result.data);
553+
// Access data directly
554+
const user = result.data;
555+
console.log(user);
556+
557+
// Or use the json() method for compatibility
558+
const userFromJson = await result.json();
559+
console.log(userFromJson);
534560
} else {
535-
console.error(\`Error \${result.status}:\`, result.error);
561+
const error = result.data;
562+
console.error(\`Error \${result.status}:\`, error);
536563
}
537564
*/
538565

0 commit comments

Comments
 (0)