typed-openapi 2.0.2 → 2.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -5,7 +5,7 @@ import {
5
5
  generateFile,
6
6
  generateTanstackQueryFile,
7
7
  mapOpenApiEndpoints
8
- } from "./chunk-KF4JBLDM.js";
8
+ } from "./chunk-KAZF7BK3.js";
9
9
  import {
10
10
  prettify
11
11
  } from "./chunk-KAEXXJ7X.js";
@@ -449,16 +449,10 @@ var parameterObjectToString = (parameters, ctx) => {
449
449
  }
450
450
  return str + "}";
451
451
  };
452
- var responseHeadersObjectToString = (responseHeaders, ctx) => {
452
+ var responseHeadersObjectToString = (responseHeaders) => {
453
453
  let str = "{";
454
454
  for (const [key, responseHeader] of Object.entries(responseHeaders)) {
455
- const value = ctx.runtime === "none" ? responseHeader.recompute((box) => {
456
- if (Box.isReference(box) && !box.params.generics && box.value !== "null") {
457
- box.value = `Schemas.${box.value}`;
458
- }
459
- return box;
460
- }).value : responseHeader.value;
461
- str += `${wrapWithQuotesIfNeeded(key.toLowerCase())}: ${value},
455
+ str += `${wrapWithQuotesIfNeeded(key.toLowerCase())}: ${responseHeader.value},
462
456
  `;
463
457
  }
464
458
  return str + "}";
@@ -503,14 +497,8 @@ var generateEndpointSchemaList = (ctx) => {
503
497
  ctx
504
498
  )},` : ""}
505
499
  }` : "parameters: never,"}
506
- response: ${ctx.runtime === "none" ? endpoint.response.recompute((box) => {
507
- if (Box.isReference(box) && !box.params.generics && box.value !== "null") {
508
- box.value = `Schemas.${box.value}`;
509
- }
510
- return box;
511
- }).value : endpoint.response.value},
512
500
  ${endpoint.responses ? `responses: ${generateResponsesObject(endpoint.responses, ctx)},` : ""}
513
- ${endpoint.responseHeaders ? `responseHeaders: ${responseHeadersObjectToString(endpoint.responseHeaders, ctx)},` : ""}
501
+ ${endpoint.responseHeaders ? `responseHeaders: ${responseHeadersObjectToString(endpoint.responseHeaders)},` : ""}
514
502
  }
515
503
  `;
516
504
  });
@@ -567,7 +555,6 @@ type RequestFormat = "json" | "form-data" | "form-url" | "binary" | "text";
567
555
 
568
556
  export type DefaultEndpoint = {
569
557
  parameters?: EndpointParameters | undefined;
570
- response: unknown;
571
558
  responses?: Record<string, unknown>;
572
559
  responseHeaders?: Record<string, unknown>;
573
560
  };
@@ -583,7 +570,6 @@ export type Endpoint<TConfig extends DefaultEndpoint = DefaultEndpoint> = {
583
570
  hasParameters: boolean;
584
571
  areParametersRequired: boolean;
585
572
  };
586
- response: TConfig["response"];
587
573
  responses?: TConfig["responses"];
588
574
  responseHeaders?: TConfig["responseHeaders"]
589
575
  };
@@ -596,49 +582,63 @@ export type SuccessStatusCode = typeof successStatusCodes[number];
596
582
  export const errorStatusCodes = [${ctx.errorStatusCodes.join(",")}] as const;
597
583
  export type ErrorStatusCode = typeof errorStatusCodes[number];
598
584
 
599
- // Error handling types
585
+ // Taken from https://github.com/unjs/fetchdts/blob/ec4eaeab5d287116171fc1efd61f4a1ad34e4609/src/fetch.ts#L3
586
+ export interface TypedHeaders<TypedHeaderValues extends Record<string, string> | unknown> extends Omit<Headers, 'append' | 'delete' | 'get' | 'getSetCookie' | 'has' | 'set' | 'forEach'> {
587
+ /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Headers/append) */
588
+ append: <Name extends Extract<keyof TypedHeaderValues, string> | string & {}> (name: Name, value: Lowercase<Name> extends keyof TypedHeaderValues ? TypedHeaderValues[Lowercase<Name>] : string) => void
589
+ /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Headers/delete) */
590
+ delete: <Name extends Extract<keyof TypedHeaderValues, string> | string & {}> (name: Name) => void
591
+ /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Headers/get) */
592
+ get: <Name extends Extract<keyof TypedHeaderValues, string> | string & {}> (name: Name) => (Lowercase<Name> extends keyof TypedHeaderValues ? TypedHeaderValues[Lowercase<Name>] : string) | null
593
+ /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Headers/getSetCookie) */
594
+ getSetCookie: () => string[]
595
+ /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Headers/has) */
596
+ has: <Name extends Extract<keyof TypedHeaderValues, string> | string & {}> (name: Name) => boolean
597
+ /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Headers/set) */
598
+ set: <Name extends Extract<keyof TypedHeaderValues, string> | string & {}> (name: Name, value: Lowercase<Name> extends keyof TypedHeaderValues ? TypedHeaderValues[Lowercase<Name>] : string) => void
599
+ forEach: (callbackfn: (value: TypedHeaderValues[keyof TypedHeaderValues] | string & {}, key: Extract<keyof TypedHeaderValues, string> | string & {}, parent: TypedHeaders<TypedHeaderValues>) => void, thisArg?: any) => void
600
+ }
601
+
600
602
  /** @see https://developer.mozilla.org/en-US/docs/Web/API/Response */
601
- interface SuccessResponse<TSuccess, TStatusCode> extends Omit<Response, "ok" | "status" | "json"> {
603
+ export interface TypedSuccessResponse<TSuccess, TStatusCode, THeaders> extends Omit<Response, "ok" | "status" | "json" | "headers"> {
602
604
  ok: true;
603
605
  status: TStatusCode;
606
+ headers: never extends THeaders ? Headers : TypedHeaders<THeaders>;
604
607
  data: TSuccess;
605
608
  /** [MDN Reference](https://developer.mozilla.org/en-US/docs/Web/API/Response/json) */
606
609
  json: () => Promise<TSuccess>;
607
610
  }
608
611
 
609
612
  /** @see https://developer.mozilla.org/en-US/docs/Web/API/Response */
610
- interface ErrorResponse<TData, TStatusCode> extends Omit<Response, "ok" | "status" | "json"> {
613
+ export interface TypedErrorResponse<TData, TStatusCode, THeaders> extends Omit<Response, "ok" | "status" | "json" | "headers"> {
611
614
  ok: false;
612
615
  status: TStatusCode;
616
+ headers: never extends THeaders ? Headers : TypedHeaders<THeaders>;
613
617
  data: TData;
614
618
  /** [MDN Reference](https://developer.mozilla.org/en-US/docs/Web/API/Response/json) */
615
619
  json: () => Promise<TData>;
616
620
  }
617
621
 
618
- export type TypedApiResponse<TSuccess, TAllResponses extends Record<string | number, unknown> = {}> =
619
- (keyof TAllResponses extends never
620
- ? SuccessResponse<TSuccess, number>
621
- : {
622
- [K in keyof TAllResponses]: K extends string
623
- ? K extends \`\${infer TStatusCode extends number}\`
624
- ? TStatusCode extends SuccessStatusCode
625
- ? SuccessResponse<TSuccess, TStatusCode>
626
- : ErrorResponse<TAllResponses[K], TStatusCode>
627
- : never
628
- : K extends number
629
- ? K extends SuccessStatusCode
630
- ? SuccessResponse<TSuccess, K>
631
- : ErrorResponse<TAllResponses[K], K>
632
- : never;
633
- }[keyof TAllResponses]);
622
+ export type TypedApiResponse<TAllResponses extends Record<string | number, unknown> = {}, THeaders = {}> =
623
+ ({
624
+ [K in keyof TAllResponses]: K extends string
625
+ ? K extends \`\${infer TStatusCode extends number}\`
626
+ ? TStatusCode extends SuccessStatusCode
627
+ ? TypedSuccessResponse<TAllResponses[K], TStatusCode, K extends keyof THeaders ? THeaders[K] : never>
628
+ : TypedErrorResponse<TAllResponses[K], TStatusCode, K extends keyof THeaders ? THeaders[K] : never>
629
+ : never
630
+ : K extends number
631
+ ? K extends SuccessStatusCode
632
+ ? TypedSuccessResponse<TAllResponses[K], K, K extends keyof THeaders ? THeaders[K] : never>
633
+ : TypedErrorResponse<TAllResponses[K], K, K extends keyof THeaders ? THeaders[K] : never>
634
+ : never;
635
+ }[keyof TAllResponses]);
634
636
 
635
- export type SafeApiResponse<TEndpoint> = TEndpoint extends { response: infer TSuccess; responses: infer TResponses }
637
+ export type SafeApiResponse<TEndpoint> = TEndpoint extends { responses: infer TResponses }
636
638
  ? TResponses extends Record<string, unknown>
637
- ? TypedApiResponse<TSuccess, TResponses>
638
- : SuccessResponse<TSuccess, number>
639
- : TEndpoint extends { response: infer TSuccess }
640
- ? SuccessResponse<TSuccess, number>
641
- : never;
639
+ ? TypedApiResponse<TResponses, TEndpoint extends { responseHeaders: infer THeaders } ? THeaders : never>
640
+ : never
641
+ : never
642
642
 
643
643
  export type InferResponseByStatus<TEndpoint, TStatusCode> = Extract<SafeApiResponse<TEndpoint>, { status: TStatusCode }>
644
644
 
@@ -653,9 +653,9 @@ type MaybeOptionalArg<T> = RequiredKeys<T> extends never ? [config?: T] : [confi
653
653
  const apiClient = `
654
654
  // <TypedResponseError>
655
655
  export class TypedResponseError extends Error {
656
- response: ErrorResponse<unknown, ErrorStatusCode>;
656
+ response: TypedErrorResponse<unknown, ErrorStatusCode, unknown>;
657
657
  status: number;
658
- constructor(response: ErrorResponse<unknown, ErrorStatusCode>) {
658
+ constructor(response: TypedErrorResponse<unknown, ErrorStatusCode, unknown>) {
659
659
  super(\`HTTP \${response.status}: \${response.statusText}\`);
660
660
  this.name = 'TypedResponseError';
661
661
  this.response = response;
@@ -691,7 +691,13 @@ export class ApiClient {
691
691
  ${method}<Path extends keyof ${capitalizedMethod}Endpoints, TEndpoint extends ${capitalizedMethod}Endpoints[Path]>(
692
692
  path: Path,
693
693
  ...params: MaybeOptionalArg<${match(ctx.runtime).with("zod", "yup", () => infer(`TEndpoint["parameters"]`)).with("arktype", "io-ts", "typebox", "valibot", () => infer(`TEndpoint`) + `["parameters"]`).otherwise(() => `TEndpoint["parameters"]`)} & { withResponse?: false; throwOnStatusError?: boolean }>
694
- ): Promise<${match(ctx.runtime).with("zod", "yup", () => infer(`TEndpoint["response"]`)).with("arktype", "io-ts", "typebox", "valibot", () => infer(`TEndpoint`) + `["response"]`).otherwise(() => `TEndpoint["response"]`)}>;
694
+ ): Promise<${match(ctx.runtime).with("zod", "yup", () => infer(`InferResponseByStatus<TEndpoint, SuccessStatusCode>`)).with(
695
+ "arktype",
696
+ "io-ts",
697
+ "typebox",
698
+ "valibot",
699
+ () => `InferResponseByStatus<${infer(`TEndpoint`)}, SuccessStatusCode>["data"]`
700
+ ).otherwise(() => `InferResponseByStatus<TEndpoint, SuccessStatusCode>["data"]`)}>;
695
701
 
696
702
  ${method}<Path extends keyof ${capitalizedMethod}Endpoints, TEndpoint extends ${capitalizedMethod}Endpoints[Path]>(
697
703
  path: Path,
@@ -721,7 +727,13 @@ export class ApiClient {
721
727
  return withResponse ? typedResponse : data;
722
728
  });
723
729
 
724
- return promise ${match(ctx.runtime).with("zod", "yup", () => `as Promise<${infer(`TEndpoint["response"]`)}>`).with("arktype", "io-ts", "typebox", "valibot", () => `as Promise<${infer(`TEndpoint`) + `["response"]`}>`).otherwise(() => `as Promise<TEndpoint["response"]>`)}
730
+ return promise ${match(ctx.runtime).with("zod", "yup", () => `as Promise<${infer(`InferResponseByStatus<TEndpoint, SuccessStatusCode>`)}>`).with(
731
+ "arktype",
732
+ "io-ts",
733
+ "typebox",
734
+ "valibot",
735
+ () => `as Promise<InferResponseByStatus<${infer(`TEndpoint`)}, SuccessStatusCode>["data"]>`
736
+ ).otherwise(() => `as Promise<InferResponseByStatus<TEndpoint, SuccessStatusCode>["data"]>`)}
725
737
  }
726
738
  // </ApiClient.${method}>
727
739
  ` : "";
@@ -1082,7 +1094,6 @@ var mapOpenApiEndpoints = (doc, options) => {
1082
1094
  method,
1083
1095
  path,
1084
1096
  requestFormat: "json",
1085
- response: openApiSchemaToTs({ schema: {}, ctx }),
1086
1097
  meta: {
1087
1098
  alias,
1088
1099
  areParametersRequired: false,
@@ -1124,11 +1135,11 @@ var mapOpenApiEndpoints = (doc, options) => {
1124
1135
  if (operation.requestBody) {
1125
1136
  endpoint.meta.hasParameters = true;
1126
1137
  const requestBody = refs.unwrap(operation.requestBody ?? {});
1127
- const content2 = requestBody.content;
1128
- const matchingMediaType = Object.keys(content2).find(isAllowedParamMediaTypes);
1129
- if (matchingMediaType && content2[matchingMediaType]) {
1138
+ const content = requestBody.content;
1139
+ const matchingMediaType = Object.keys(content).find(isAllowedParamMediaTypes);
1140
+ if (matchingMediaType && content[matchingMediaType]) {
1130
1141
  params.body = openApiSchemaToTs({
1131
- schema: content2[matchingMediaType]?.schema ?? {},
1142
+ schema: content[matchingMediaType]?.schema ?? {},
1132
1143
  ctx
1133
1144
  });
1134
1145
  }
@@ -1159,67 +1170,64 @@ var mapOpenApiEndpoints = (doc, options) => {
1159
1170
  }
1160
1171
  endpoint.parameters = Object.keys(params).length ? params : void 0;
1161
1172
  }
1162
- let responseObject;
1163
1173
  const allResponses = {};
1174
+ const allHeaders = {};
1164
1175
  Object.entries(operation.responses ?? {}).map(([status, responseOrRef]) => {
1165
- const statusCode = Number(status);
1166
1176
  const responseObj = refs.unwrap(responseOrRef);
1167
- const content2 = responseObj?.content;
1168
- if (content2) {
1169
- const matchingMediaType = Object.keys(content2).find(isResponseMediaType);
1170
- if (matchingMediaType && content2[matchingMediaType]) {
1171
- allResponses[status] = openApiSchemaToTs({
1172
- schema: content2[matchingMediaType]?.schema ?? {},
1173
- ctx
1174
- });
1177
+ const content = responseObj?.content;
1178
+ const mediaTypes = Object.keys(content ?? {}).filter(isResponseMediaType);
1179
+ if (content && mediaTypes.length) {
1180
+ mediaTypes.forEach((mediaType) => {
1181
+ const schema = content[mediaType] ? content[mediaType].schema ?? {} : {};
1182
+ const t2 = createBoxFactory(schema, ctx);
1183
+ const mediaTypeResponse = openApiSchemaToTs({ schema, ctx });
1184
+ if (allResponses[status]) {
1185
+ allResponses[status] = t2.union([
1186
+ ...Array.isArray(allResponses[status]) ? allResponses[status] : [allResponses[status]],
1187
+ mediaTypeResponse
1188
+ ]);
1189
+ } else {
1190
+ allResponses[status] = mediaTypeResponse;
1191
+ }
1192
+ });
1193
+ } else {
1194
+ const schema = {};
1195
+ const unknown = openApiSchemaToTs({ schema: {}, ctx });
1196
+ const t2 = createBoxFactory(schema, ctx);
1197
+ if (allResponses[status]) {
1198
+ allResponses[status] = t2.union([
1199
+ ...Array.isArray(allResponses[status]) ? allResponses[status] : [allResponses[status]],
1200
+ unknown
1201
+ ]);
1175
1202
  } else {
1176
- allResponses[status] = openApiSchemaToTs({ schema: {}, ctx });
1203
+ allResponses[status] = unknown;
1177
1204
  }
1178
- } else {
1179
- allResponses[status] = openApiSchemaToTs({ schema: {}, ctx });
1180
1205
  }
1181
- if (statusCode >= 200 && statusCode < 300 && !responseObject) {
1182
- responseObject = responseObj;
1183
- }
1184
- });
1185
- if (!responseObject && operation.responses?.default) {
1186
- responseObject = refs.unwrap(operation.responses.default);
1187
- if (!allResponses["default"]) {
1188
- const content2 = responseObject?.content;
1189
- if (content2) {
1190
- const matchingMediaType = Object.keys(content2).find(isResponseMediaType);
1191
- if (matchingMediaType && content2[matchingMediaType]) {
1192
- allResponses["default"] = openApiSchemaToTs({
1193
- schema: content2[matchingMediaType]?.schema ?? {},
1194
- ctx
1195
- });
1196
- }
1206
+ const headers = responseObj?.headers;
1207
+ const t = createBoxFactory(
1208
+ { type: "object", properties: headers ?? {}, required: Object.keys(headers ?? {}) },
1209
+ ctx
1210
+ );
1211
+ if (headers) {
1212
+ const mappedHeaders = Object.entries(headers).reduce(
1213
+ (acc, [name, headerOrRef]) => {
1214
+ const header = refs.unwrap(headerOrRef);
1215
+ const box = openApiSchemaToTs({ schema: header.schema ?? {}, ctx });
1216
+ acc[name] = box;
1217
+ return acc;
1218
+ },
1219
+ {}
1220
+ );
1221
+ if (Object.keys(mappedHeaders).length) {
1222
+ allHeaders[status] = t.object(mappedHeaders);
1197
1223
  }
1198
1224
  }
1199
- }
1225
+ });
1200
1226
  if (Object.keys(allResponses).length > 0) {
1201
1227
  endpoint.responses = allResponses;
1202
1228
  }
1203
- const content = responseObject?.content;
1204
- if (content) {
1205
- const matchingMediaType = Object.keys(content).find(isResponseMediaType);
1206
- if (matchingMediaType && content[matchingMediaType]) {
1207
- endpoint.response = openApiSchemaToTs({
1208
- schema: content[matchingMediaType]?.schema ?? {},
1209
- ctx
1210
- });
1211
- }
1212
- }
1213
- const headers = responseObject?.headers;
1214
- if (headers) {
1215
- endpoint.responseHeaders = Object.entries(headers).reduce(
1216
- (acc, [name, headerOrRef]) => {
1217
- const header = refs.unwrap(headerOrRef);
1218
- acc[name] = openApiSchemaToTs({ schema: header.schema ?? {}, ctx });
1219
- return acc;
1220
- },
1221
- {}
1222
- );
1229
+ if (Object.keys(allHeaders).length) {
1230
+ endpoint.responseHeaders = allHeaders;
1223
1231
  }
1224
1232
  endpointList.push(endpoint);
1225
1233
  });
@@ -1325,7 +1333,7 @@ var generateTanstackQueryFile = async (ctx) => {
1325
1333
  withResponse: false as const
1326
1334
  };
1327
1335
  const res = await this.client.${method}(path, requestParams);
1328
- return res as TEndpoint["response"];
1336
+ return res as InferResponseByStatus<TEndpoint, SuccessStatusCode>["data"];
1329
1337
  },
1330
1338
  queryKey: queryKey
1331
1339
  }),
@@ -1340,7 +1348,7 @@ var generateTanstackQueryFile = async (ctx) => {
1340
1348
  withResponse: false as const
1341
1349
  };
1342
1350
  const res = await this.client.${method}(path, requestParams);
1343
- return res as TEndpoint["response"];
1351
+ return res as InferResponseByStatus<TEndpoint, SuccessStatusCode>["data"];
1344
1352
  }
1345
1353
  }
1346
1354
  };
package/dist/cli.js CHANGED
@@ -1,9 +1,9 @@
1
1
  import {
2
2
  generateClientFiles
3
- } from "./chunk-Q6LQYDKL.js";
3
+ } from "./chunk-CO4NONVX.js";
4
4
  import {
5
5
  allowedRuntimes
6
- } from "./chunk-KF4JBLDM.js";
6
+ } from "./chunk-KAZF7BK3.js";
7
7
  import "./chunk-KAEXXJ7X.js";
8
8
 
9
9
  // src/cli.ts
package/dist/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { ReferenceObject } from 'openapi3-ts/oas31';
2
- import { S as StringOrBox, O as OpenapiSchemaConvertContext, L as LibSchemaObject, B as BoxFactory, m as mapOpenApiEndpoints, N as NameTransformOptions, a as OpenapiSchemaConvertArgs, b as Box, A as AnyBoxDef } from './types-DsI2d-HE.js';
3
- export { q as AnyBox, j as BoxArray, f as BoxDefinition, i as BoxIntersection, o as BoxKeyword, n as BoxLiteral, p as BoxObject, k as BoxOptional, g as BoxParams, l as BoxRef, h as BoxUnion, c as Endpoint, E as EndpointParameters, F as FactoryCreator, G as GenericFactory, M as Method, R as RefInfo, e as RefResolver, W as WithSchema, d as createRefResolver } from './types-DsI2d-HE.js';
2
+ import { S as StringOrBox, O as OpenapiSchemaConvertContext, L as LibSchemaObject, B as BoxFactory, m as mapOpenApiEndpoints, N as NameTransformOptions, a as OpenapiSchemaConvertArgs, b as Box, A as AnyBoxDef } from './types-BOJSTQwz.js';
3
+ export { q as AnyBox, j as BoxArray, f as BoxDefinition, i as BoxIntersection, o as BoxKeyword, n as BoxLiteral, p as BoxObject, k as BoxOptional, g as BoxParams, l as BoxRef, h as BoxUnion, c as Endpoint, E as EndpointParameters, F as FactoryCreator, G as GenericFactory, M as Method, R as RefInfo, e as RefResolver, W as WithSchema, d as createRefResolver } from './types-BOJSTQwz.js';
4
4
  import * as arktype_internal_methods_string_ts from 'arktype/internal/methods/string.ts';
5
5
  import * as Codegen from '@sinclair/typebox-codegen';
6
6
  import 'openapi3-ts/oas30';
package/dist/index.js CHANGED
@@ -8,7 +8,7 @@ import {
8
8
  openApiSchemaToTs,
9
9
  tsFactory,
10
10
  unwrap
11
- } from "./chunk-KF4JBLDM.js";
11
+ } from "./chunk-KAZF7BK3.js";
12
12
  import "./chunk-KAEXXJ7X.js";
13
13
  export {
14
14
  createBoxFactory,
@@ -1,5 +1,5 @@
1
1
  import * as arktype_internal_methods_object_ts from 'arktype/internal/methods/object.ts';
2
- import { N as NameTransformOptions } from './types-DsI2d-HE.js';
2
+ import { N as NameTransformOptions } from './types-BOJSTQwz.js';
3
3
  import 'openapi3-ts/oas31';
4
4
  import 'openapi3-ts/oas30';
5
5
 
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  generateClientFiles
3
- } from "./chunk-Q6LQYDKL.js";
4
- import "./chunk-KF4JBLDM.js";
3
+ } from "./chunk-CO4NONVX.js";
4
+ import "./chunk-KAZF7BK3.js";
5
5
  import "./chunk-KAEXXJ7X.js";
6
6
  export {
7
7
  generateClientFiles
@@ -99,9 +99,8 @@ type EndpointParameters = {
99
99
  type RequestFormat = "json" | "form-data" | "form-url" | "binary" | "text";
100
100
  type DefaultEndpoint = {
101
101
  parameters?: EndpointParameters | undefined;
102
- response: AnyBox;
103
102
  responses?: Record<string, AnyBox>;
104
- responseHeaders?: Record<string, AnyBox>;
103
+ responseHeaders?: Record<string, Box<BoxObject>>;
105
104
  };
106
105
  type Endpoint<TConfig extends DefaultEndpoint = DefaultEndpoint> = {
107
106
  operation: OperationObject;
@@ -114,7 +113,6 @@ type Endpoint<TConfig extends DefaultEndpoint = DefaultEndpoint> = {
114
113
  hasParameters: boolean;
115
114
  areParametersRequired: boolean;
116
115
  };
117
- response: TConfig["response"];
118
116
  responses?: TConfig["responses"];
119
117
  responseHeaders?: TConfig["responseHeaders"];
120
118
  };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "typed-openapi",
3
3
  "type": "module",
4
- "version": "2.0.2",
4
+ "version": "2.1.1",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.js",
7
7
  "exports": {
@@ -36,7 +36,8 @@
36
36
  "msw": "2.10.5",
37
37
  "tsup": "^8.4.0",
38
38
  "typescript": "^5.8.3",
39
- "vitest": "^3.1.3"
39
+ "vitest": "^3.1.3",
40
+ "zod": "3.21.4"
40
41
  },
41
42
  "files": [
42
43
  "src",
package/src/generator.ts CHANGED
@@ -6,7 +6,7 @@ import * as Codegen from "@sinclair/typebox-codegen";
6
6
  import { match } from "ts-pattern";
7
7
  import { type } from "arktype";
8
8
  import { wrapWithQuotesIfNeeded } from "./string-utils.ts";
9
- import type { NameTransformOptions } from "./types.ts";
9
+ import type { BoxObject, NameTransformOptions } from "./types.ts";
10
10
 
11
11
  // Default success status codes (2xx and 3xx ranges)
12
12
  export const DEFAULT_SUCCESS_STATUS_CODES = [
@@ -170,20 +170,10 @@ const parameterObjectToString = (parameters: Box<AnyBoxDef> | Record<string, Any
170
170
  return str + "}";
171
171
  };
172
172
 
173
- const responseHeadersObjectToString = (responseHeaders: Record<string, AnyBox>, ctx: GeneratorContext) => {
173
+ const responseHeadersObjectToString = (responseHeaders: Record<string, Box<BoxObject>>) => {
174
174
  let str = "{";
175
175
  for (const [key, responseHeader] of Object.entries(responseHeaders)) {
176
- const value =
177
- ctx.runtime === "none"
178
- ? responseHeader.recompute((box) => {
179
- if (Box.isReference(box) && !box.params.generics && box.value !== "null") {
180
- box.value = `Schemas.${box.value}`;
181
- }
182
-
183
- return box;
184
- }).value
185
- : responseHeader.value;
186
- str += `${wrapWithQuotesIfNeeded(key.toLowerCase())}: ${value},\n`;
176
+ str += `${wrapWithQuotesIfNeeded(key.toLowerCase())}: ${responseHeader.value},\n`;
187
177
  }
188
178
  return str + "}";
189
179
  };
@@ -242,21 +232,10 @@ const generateEndpointSchemaList = (ctx: GeneratorContext) => {
242
232
  }`
243
233
  : "parameters: never,"
244
234
  }
245
- response: ${
246
- ctx.runtime === "none"
247
- ? endpoint.response.recompute((box) => {
248
- if (Box.isReference(box) && !box.params.generics && box.value !== "null") {
249
- box.value = `Schemas.${box.value}`;
250
- }
251
-
252
- return box;
253
- }).value
254
- : endpoint.response.value
255
- },
256
235
  ${endpoint.responses ? `responses: ${generateResponsesObject(endpoint.responses, ctx)},` : ""}
257
236
  ${
258
237
  endpoint.responseHeaders
259
- ? `responseHeaders: ${responseHeadersObjectToString(endpoint.responseHeaders, ctx)},`
238
+ ? `responseHeaders: ${responseHeadersObjectToString(endpoint.responseHeaders)},`
260
239
  : ""
261
240
  }
262
241
  }\n`;
@@ -331,7 +310,6 @@ type RequestFormat = "json" | "form-data" | "form-url" | "binary" | "text";
331
310
 
332
311
  export type DefaultEndpoint = {
333
312
  parameters?: EndpointParameters | undefined;
334
- response: unknown;
335
313
  responses?: Record<string, unknown>;
336
314
  responseHeaders?: Record<string, unknown>;
337
315
  };
@@ -347,7 +325,6 @@ export type Endpoint<TConfig extends DefaultEndpoint = DefaultEndpoint> = {
347
325
  hasParameters: boolean;
348
326
  areParametersRequired: boolean;
349
327
  };
350
- response: TConfig["response"];
351
328
  responses?: TConfig["responses"];
352
329
  responseHeaders?: TConfig["responseHeaders"]
353
330
  };
@@ -360,49 +337,63 @@ export type SuccessStatusCode = typeof successStatusCodes[number];
360
337
  export const errorStatusCodes = [${ctx.errorStatusCodes.join(",")}] as const;
361
338
  export type ErrorStatusCode = typeof errorStatusCodes[number];
362
339
 
363
- // Error handling types
340
+ // Taken from https://github.com/unjs/fetchdts/blob/ec4eaeab5d287116171fc1efd61f4a1ad34e4609/src/fetch.ts#L3
341
+ export interface TypedHeaders<TypedHeaderValues extends Record<string, string> | unknown> extends Omit<Headers, 'append' | 'delete' | 'get' | 'getSetCookie' | 'has' | 'set' | 'forEach'> {
342
+ /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Headers/append) */
343
+ append: <Name extends Extract<keyof TypedHeaderValues, string> | string & {}> (name: Name, value: Lowercase<Name> extends keyof TypedHeaderValues ? TypedHeaderValues[Lowercase<Name>] : string) => void
344
+ /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Headers/delete) */
345
+ delete: <Name extends Extract<keyof TypedHeaderValues, string> | string & {}> (name: Name) => void
346
+ /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Headers/get) */
347
+ get: <Name extends Extract<keyof TypedHeaderValues, string> | string & {}> (name: Name) => (Lowercase<Name> extends keyof TypedHeaderValues ? TypedHeaderValues[Lowercase<Name>] : string) | null
348
+ /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Headers/getSetCookie) */
349
+ getSetCookie: () => string[]
350
+ /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Headers/has) */
351
+ has: <Name extends Extract<keyof TypedHeaderValues, string> | string & {}> (name: Name) => boolean
352
+ /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Headers/set) */
353
+ set: <Name extends Extract<keyof TypedHeaderValues, string> | string & {}> (name: Name, value: Lowercase<Name> extends keyof TypedHeaderValues ? TypedHeaderValues[Lowercase<Name>] : string) => void
354
+ forEach: (callbackfn: (value: TypedHeaderValues[keyof TypedHeaderValues] | string & {}, key: Extract<keyof TypedHeaderValues, string> | string & {}, parent: TypedHeaders<TypedHeaderValues>) => void, thisArg?: any) => void
355
+ }
356
+
364
357
  /** @see https://developer.mozilla.org/en-US/docs/Web/API/Response */
365
- interface SuccessResponse<TSuccess, TStatusCode> extends Omit<Response, "ok" | "status" | "json"> {
358
+ export interface TypedSuccessResponse<TSuccess, TStatusCode, THeaders> extends Omit<Response, "ok" | "status" | "json" | "headers"> {
366
359
  ok: true;
367
360
  status: TStatusCode;
361
+ headers: never extends THeaders ? Headers : TypedHeaders<THeaders>;
368
362
  data: TSuccess;
369
363
  /** [MDN Reference](https://developer.mozilla.org/en-US/docs/Web/API/Response/json) */
370
364
  json: () => Promise<TSuccess>;
371
365
  }
372
366
 
373
367
  /** @see https://developer.mozilla.org/en-US/docs/Web/API/Response */
374
- interface ErrorResponse<TData, TStatusCode> extends Omit<Response, "ok" | "status" | "json"> {
368
+ export interface TypedErrorResponse<TData, TStatusCode, THeaders> extends Omit<Response, "ok" | "status" | "json" | "headers"> {
375
369
  ok: false;
376
370
  status: TStatusCode;
371
+ headers: never extends THeaders ? Headers : TypedHeaders<THeaders>;
377
372
  data: TData;
378
373
  /** [MDN Reference](https://developer.mozilla.org/en-US/docs/Web/API/Response/json) */
379
374
  json: () => Promise<TData>;
380
375
  }
381
376
 
382
- export type TypedApiResponse<TSuccess, TAllResponses extends Record<string | number, unknown> = {}> =
383
- (keyof TAllResponses extends never
384
- ? SuccessResponse<TSuccess, number>
385
- : {
386
- [K in keyof TAllResponses]: K extends string
387
- ? K extends \`\${infer TStatusCode extends number}\`
388
- ? TStatusCode extends SuccessStatusCode
389
- ? SuccessResponse<TSuccess, TStatusCode>
390
- : ErrorResponse<TAllResponses[K], TStatusCode>
391
- : never
392
- : K extends number
393
- ? K extends SuccessStatusCode
394
- ? SuccessResponse<TSuccess, K>
395
- : ErrorResponse<TAllResponses[K], K>
396
- : never;
397
- }[keyof TAllResponses]);
398
-
399
- export type SafeApiResponse<TEndpoint> = TEndpoint extends { response: infer TSuccess; responses: infer TResponses }
377
+ export type TypedApiResponse<TAllResponses extends Record<string | number, unknown> = {}, THeaders = {}> =
378
+ ({
379
+ [K in keyof TAllResponses]: K extends string
380
+ ? K extends \`\${infer TStatusCode extends number}\`
381
+ ? TStatusCode extends SuccessStatusCode
382
+ ? TypedSuccessResponse<TAllResponses[K], TStatusCode, K extends keyof THeaders ? THeaders[K] : never>
383
+ : TypedErrorResponse<TAllResponses[K], TStatusCode, K extends keyof THeaders ? THeaders[K] : never>
384
+ : never
385
+ : K extends number
386
+ ? K extends SuccessStatusCode
387
+ ? TypedSuccessResponse<TAllResponses[K], K, K extends keyof THeaders ? THeaders[K] : never>
388
+ : TypedErrorResponse<TAllResponses[K], K, K extends keyof THeaders ? THeaders[K] : never>
389
+ : never;
390
+ }[keyof TAllResponses]);
391
+
392
+ export type SafeApiResponse<TEndpoint> = TEndpoint extends { responses: infer TResponses }
400
393
  ? TResponses extends Record<string, unknown>
401
- ? TypedApiResponse<TSuccess, TResponses>
402
- : SuccessResponse<TSuccess, number>
403
- : TEndpoint extends { response: infer TSuccess }
404
- ? SuccessResponse<TSuccess, number>
405
- : never;
394
+ ? TypedApiResponse<TResponses, TEndpoint extends { responseHeaders: infer THeaders } ? THeaders : never>
395
+ : never
396
+ : never
406
397
 
407
398
  export type InferResponseByStatus<TEndpoint, TStatusCode> = Extract<SafeApiResponse<TEndpoint>, { status: TStatusCode }>
408
399
 
@@ -418,9 +409,9 @@ type MaybeOptionalArg<T> = RequiredKeys<T> extends never ? [config?: T] : [confi
418
409
  const apiClient = `
419
410
  // <TypedResponseError>
420
411
  export class TypedResponseError extends Error {
421
- response: ErrorResponse<unknown, ErrorStatusCode>;
412
+ response: TypedErrorResponse<unknown, ErrorStatusCode, unknown>;
422
413
  status: number;
423
- constructor(response: ErrorResponse<unknown, ErrorStatusCode>) {
414
+ constructor(response: TypedErrorResponse<unknown, ErrorStatusCode, unknown>) {
424
415
  super(\`HTTP \${response.status}: \${response.statusText}\`);
425
416
  this.name = 'TypedResponseError';
426
417
  this.response = response;
@@ -463,9 +454,15 @@ export class ApiClient {
463
454
  .with("arktype", "io-ts", "typebox", "valibot", () => infer(`TEndpoint`) + `["parameters"]`)
464
455
  .otherwise(() => `TEndpoint["parameters"]`)} & { withResponse?: false; throwOnStatusError?: boolean }>
465
456
  ): Promise<${match(ctx.runtime)
466
- .with("zod", "yup", () => infer(`TEndpoint["response"]`))
467
- .with("arktype", "io-ts", "typebox", "valibot", () => infer(`TEndpoint`) + `["response"]`)
468
- .otherwise(() => `TEndpoint["response"]`)}>;
457
+ .with("zod", "yup", () => infer(`InferResponseByStatus<TEndpoint, SuccessStatusCode>`))
458
+ .with(
459
+ "arktype",
460
+ "io-ts",
461
+ "typebox",
462
+ "valibot",
463
+ () => `InferResponseByStatus<${infer(`TEndpoint`)}, SuccessStatusCode>["data"]`,
464
+ )
465
+ .otherwise(() => `InferResponseByStatus<TEndpoint, SuccessStatusCode>["data"]`)}>;
469
466
 
470
467
  ${method}<Path extends keyof ${capitalizedMethod}Endpoints, TEndpoint extends ${capitalizedMethod}Endpoints[Path]>(
471
468
  path: Path,
@@ -499,9 +496,15 @@ export class ApiClient {
499
496
  });
500
497
 
501
498
  return promise ${match(ctx.runtime)
502
- .with("zod", "yup", () => `as Promise<${infer(`TEndpoint["response"]`)}>`)
503
- .with("arktype", "io-ts", "typebox", "valibot", () => `as Promise<${infer(`TEndpoint`) + `["response"]`}>`)
504
- .otherwise(() => `as Promise<TEndpoint["response"]>`)}
499
+ .with("zod", "yup", () => `as Promise<${infer(`InferResponseByStatus<TEndpoint, SuccessStatusCode>`)}>`)
500
+ .with(
501
+ "arktype",
502
+ "io-ts",
503
+ "typebox",
504
+ "valibot",
505
+ () => `as Promise<InferResponseByStatus<${infer(`TEndpoint`)}, SuccessStatusCode>["data"]>`,
506
+ )
507
+ .otherwise(() => `as Promise<InferResponseByStatus<TEndpoint, SuccessStatusCode>["data"]>`)}
505
508
  }
506
509
  // </ApiClient.${method}>
507
510
  `
@@ -6,7 +6,7 @@ import { createBoxFactory } from "./box-factory.ts";
6
6
  import { openApiSchemaToTs } from "./openapi-schema-to-ts.ts";
7
7
  import { createRefResolver } from "./ref-resolver.ts";
8
8
  import { tsFactory } from "./ts-factory.ts";
9
- import { AnyBox, BoxRef, OpenapiSchemaConvertContext } from "./types.ts";
9
+ import { AnyBox, BoxRef, OpenapiSchemaConvertContext, type BoxObject, type LibSchemaObject } from "./types.ts";
10
10
  import { pathToVariableName } from "./string-utils.ts";
11
11
  import { NameTransformOptions } from "./types.ts";
12
12
  import { match, P } from "ts-pattern";
@@ -33,7 +33,6 @@ export const mapOpenApiEndpoints = (doc: OpenAPIObject, options?: { nameTransfor
33
33
  method: method as Method,
34
34
  path,
35
35
  requestFormat: "json",
36
- response: openApiSchemaToTs({ schema: {}, ctx }),
37
36
  meta: {
38
37
  alias,
39
38
  areParametersRequired: false,
@@ -131,86 +130,81 @@ export const mapOpenApiEndpoints = (doc: OpenAPIObject, options?: { nameTransfor
131
130
  }
132
131
  }
133
132
 
134
- // No need to pass empty objects, it's confusing
135
133
  endpoint.parameters = Object.keys(params).length ? (params as any as EndpointParameters) : undefined;
136
134
  }
137
135
 
138
- // Match the first 2xx-3xx response found, or fallback to default one otherwise
139
- let responseObject: ResponseObject | undefined;
140
136
  const allResponses: Record<string, AnyBox> = {};
137
+ const allHeaders: Record<string, Box<BoxObject>> = {};
141
138
 
142
139
  Object.entries(operation.responses ?? {}).map(([status, responseOrRef]) => {
143
- const statusCode = Number(status);
144
140
  const responseObj = refs.unwrap<ResponseObject>(responseOrRef);
145
141
 
146
142
  // Collect all responses for error handling
147
143
  const content = responseObj?.content;
148
- if (content) {
149
- const matchingMediaType = Object.keys(content).find(isResponseMediaType);
150
- if (matchingMediaType && content[matchingMediaType]) {
151
- allResponses[status] = openApiSchemaToTs({
152
- schema: content[matchingMediaType]?.schema ?? {},
153
- ctx,
154
- });
155
- } else {
144
+ const mediaTypes = Object.keys(content ?? {}).filter(isResponseMediaType);
145
+ if (content && mediaTypes.length) {
146
+ mediaTypes.forEach((mediaType) => {
156
147
  // If no JSON content, use unknown type
157
- allResponses[status] = openApiSchemaToTs({ schema: {}, ctx });
158
- }
148
+ const schema = content[mediaType] ? (content[mediaType].schema ?? {}) : {};
149
+ const t = createBoxFactory(schema as LibSchemaObject, ctx);
150
+ const mediaTypeResponse = openApiSchemaToTs({ schema, ctx });
151
+
152
+ if (allResponses[status]) {
153
+ allResponses[status] = t.union([
154
+ ...(Array.isArray(allResponses[status]) ? allResponses[status] : [allResponses[status]]),
155
+ mediaTypeResponse,
156
+ ]);
157
+ } else {
158
+ allResponses[status] = mediaTypeResponse;
159
+ }
160
+ });
159
161
  } else {
160
162
  // If no content defined, use unknown type
161
- allResponses[status] = openApiSchemaToTs({ schema: {}, ctx });
162
- }
163
-
164
- // Keep the current logic for the main response (first 2xx-3xx)
165
- if (statusCode >= 200 && statusCode < 300 && !responseObject) {
166
- responseObject = responseObj;
163
+ const schema = {};
164
+ const unknown = openApiSchemaToTs({ schema: {}, ctx });
165
+ const t = createBoxFactory(schema as LibSchemaObject, ctx);
166
+
167
+ if (allResponses[status]) {
168
+ allResponses[status] = t.union([
169
+ ...(Array.isArray(allResponses[status]) ? allResponses[status] : [allResponses[status]]),
170
+ unknown,
171
+ ]);
172
+ } else {
173
+ allResponses[status] = unknown;
174
+ }
167
175
  }
168
- });
169
176
 
170
- if (!responseObject && operation.responses?.default) {
171
- responseObject = refs.unwrap(operation.responses.default);
172
- // Also add default to all responses if not already covered
173
- if (!allResponses["default"]) {
174
- const content = responseObject?.content;
175
- if (content) {
176
- const matchingMediaType = Object.keys(content).find(isResponseMediaType);
177
- if (matchingMediaType && content[matchingMediaType]) {
178
- allResponses["default"] = openApiSchemaToTs({
179
- schema: content[matchingMediaType]?.schema ?? {},
180
- ctx,
181
- });
182
- }
177
+ // Map response headers
178
+ const headers = responseObj?.headers;
179
+ const t = createBoxFactory(
180
+ { type: "object", properties: (headers ?? {}) as never, required: Object.keys(headers ?? {}) },
181
+ ctx,
182
+ );
183
+ if (headers) {
184
+ const mappedHeaders = Object.entries(headers).reduce(
185
+ (acc, [name, headerOrRef]) => {
186
+ const header = refs.unwrap(headerOrRef);
187
+ const box = openApiSchemaToTs({ schema: header.schema ?? {}, ctx });
188
+ acc[name] = box;
189
+
190
+ return acc;
191
+ },
192
+ {} as Record<string, Box>,
193
+ );
194
+
195
+ if (Object.keys(mappedHeaders).length) {
196
+ allHeaders[status] = t.object(mappedHeaders);
183
197
  }
184
198
  }
185
- }
199
+ });
186
200
 
187
201
  // Set the responses collection
188
202
  if (Object.keys(allResponses).length > 0) {
189
203
  endpoint.responses = allResponses;
190
204
  }
191
205
 
192
- const content = responseObject?.content;
193
- if (content) {
194
- const matchingMediaType = Object.keys(content).find(isResponseMediaType);
195
- if (matchingMediaType && content[matchingMediaType]) {
196
- endpoint.response = openApiSchemaToTs({
197
- schema: content[matchingMediaType]?.schema ?? {},
198
- ctx,
199
- });
200
- }
201
- }
202
-
203
- // Map response headers
204
- const headers = responseObject?.headers;
205
- if (headers) {
206
- endpoint.responseHeaders = Object.entries(headers).reduce(
207
- (acc, [name, headerOrRef]) => {
208
- const header = refs.unwrap(headerOrRef);
209
- acc[name] = openApiSchemaToTs({ schema: header.schema ?? {}, ctx });
210
- return acc;
211
- },
212
- {} as Record<string, Box>,
213
- );
206
+ if (Object.keys(allHeaders).length) {
207
+ endpoint.responseHeaders = allHeaders;
214
208
  }
215
209
 
216
210
  endpointList.push(endpoint);
@@ -233,7 +227,8 @@ const isAllowedParamMediaTypes = (
233
227
  allowedParamMediaTypes.includes(mediaType as any) ||
234
228
  mediaType.includes("text/");
235
229
 
236
- const isResponseMediaType = (mediaType: string) => mediaType === "*/*" || (mediaType.includes("application/") && mediaType.includes("json"));
230
+ const isResponseMediaType = (mediaType: string) =>
231
+ mediaType === "*/*" || (mediaType.includes("application/") && mediaType.includes("json"));
237
232
  const getAlias = ({ path, method, operation }: Endpoint) =>
238
233
  sanitizeName(
239
234
  (method + "_" + capitalize(operation.operationId ?? pathToVariableName(path))).replace(/-/g, "__"),
@@ -254,9 +249,8 @@ type RequestFormat = "json" | "form-data" | "form-url" | "binary" | "text";
254
249
 
255
250
  type DefaultEndpoint = {
256
251
  parameters?: EndpointParameters | undefined;
257
- response: AnyBox;
258
252
  responses?: Record<string, AnyBox>;
259
- responseHeaders?: Record<string, AnyBox>;
253
+ responseHeaders?: Record<string, Box<BoxObject>>;
260
254
  };
261
255
 
262
256
  export type Endpoint<TConfig extends DefaultEndpoint = DefaultEndpoint> = {
@@ -270,7 +264,6 @@ export type Endpoint<TConfig extends DefaultEndpoint = DefaultEndpoint> = {
270
264
  hasParameters: boolean;
271
265
  areParametersRequired: boolean;
272
266
  };
273
- response: TConfig["response"];
274
267
  responses?: TConfig["responses"];
275
268
  responseHeaders?: TConfig["responseHeaders"];
276
269
  };
@@ -95,7 +95,7 @@ export const generateTanstackQueryFile = async (ctx: GeneratorContext & { relati
95
95
  withResponse: false as const
96
96
  };
97
97
  const res = await this.client.${method}(path, requestParams);
98
- return res as TEndpoint["response"];
98
+ return res as InferResponseByStatus<TEndpoint, SuccessStatusCode>["data"];
99
99
  },
100
100
  queryKey: queryKey
101
101
  }),
@@ -110,7 +110,7 @@ export const generateTanstackQueryFile = async (ctx: GeneratorContext & { relati
110
110
  withResponse: false as const
111
111
  };
112
112
  const res = await this.client.${method}(path, requestParams);
113
- return res as TEndpoint["response"];
113
+ return res as InferResponseByStatus<TEndpoint, SuccessStatusCode>["data"];
114
114
  }
115
115
  }
116
116
  };