typed-openapi 2.0.1 → 2.1.0

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.
@@ -503,12 +503,6 @@ var generateEndpointSchemaList = (ctx) => {
503
503
  ctx
504
504
  )},` : ""}
505
505
  }` : "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
506
  ${endpoint.responses ? `responses: ${generateResponsesObject(endpoint.responses, ctx)},` : ""}
513
507
  ${endpoint.responseHeaders ? `responseHeaders: ${responseHeadersObjectToString(endpoint.responseHeaders, ctx)},` : ""}
514
508
  }
@@ -567,7 +561,6 @@ type RequestFormat = "json" | "form-data" | "form-url" | "binary" | "text";
567
561
 
568
562
  export type DefaultEndpoint = {
569
563
  parameters?: EndpointParameters | undefined;
570
- response: unknown;
571
564
  responses?: Record<string, unknown>;
572
565
  responseHeaders?: Record<string, unknown>;
573
566
  };
@@ -583,7 +576,6 @@ export type Endpoint<TConfig extends DefaultEndpoint = DefaultEndpoint> = {
583
576
  hasParameters: boolean;
584
577
  areParametersRequired: boolean;
585
578
  };
586
- response: TConfig["response"];
587
579
  responses?: TConfig["responses"];
588
580
  responseHeaders?: TConfig["responseHeaders"]
589
581
  };
@@ -596,49 +588,63 @@ export type SuccessStatusCode = typeof successStatusCodes[number];
596
588
  export const errorStatusCodes = [${ctx.errorStatusCodes.join(",")}] as const;
597
589
  export type ErrorStatusCode = typeof errorStatusCodes[number];
598
590
 
599
- // Error handling types
591
+ // Taken from https://github.com/unjs/fetchdts/blob/ec4eaeab5d287116171fc1efd61f4a1ad34e4609/src/fetch.ts#L3
592
+ export interface TypedHeaders<TypedHeaderValues extends Record<string, string> | unknown> extends Omit<Headers, 'append' | 'delete' | 'get' | 'getSetCookie' | 'has' | 'set' | 'forEach'> {
593
+ /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Headers/append) */
594
+ append: <Name extends Extract<keyof TypedHeaderValues, string> | string & {}> (name: Name, value: Lowercase<Name> extends keyof TypedHeaderValues ? TypedHeaderValues[Lowercase<Name>] : string) => void
595
+ /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Headers/delete) */
596
+ delete: <Name extends Extract<keyof TypedHeaderValues, string> | string & {}> (name: Name) => void
597
+ /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Headers/get) */
598
+ get: <Name extends Extract<keyof TypedHeaderValues, string> | string & {}> (name: Name) => (Lowercase<Name> extends keyof TypedHeaderValues ? TypedHeaderValues[Lowercase<Name>] : string) | null
599
+ /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Headers/getSetCookie) */
600
+ getSetCookie: () => string[]
601
+ /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Headers/has) */
602
+ has: <Name extends Extract<keyof TypedHeaderValues, string> | string & {}> (name: Name) => boolean
603
+ /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Headers/set) */
604
+ set: <Name extends Extract<keyof TypedHeaderValues, string> | string & {}> (name: Name, value: Lowercase<Name> extends keyof TypedHeaderValues ? TypedHeaderValues[Lowercase<Name>] : string) => void
605
+ forEach: (callbackfn: (value: TypedHeaderValues[keyof TypedHeaderValues] | string & {}, key: Extract<keyof TypedHeaderValues, string> | string & {}, parent: TypedHeaders<TypedHeaderValues>) => void, thisArg?: any) => void
606
+ }
607
+
600
608
  /** @see https://developer.mozilla.org/en-US/docs/Web/API/Response */
601
- interface SuccessResponse<TSuccess, TStatusCode> extends Omit<Response, "ok" | "status" | "json"> {
609
+ export interface TypedSuccessResponse<TSuccess, TStatusCode, THeaders> extends Omit<Response, "ok" | "status" | "json" | "headers"> {
602
610
  ok: true;
603
611
  status: TStatusCode;
612
+ headers: never extends THeaders ? Headers : TypedHeaders<THeaders>;
604
613
  data: TSuccess;
605
614
  /** [MDN Reference](https://developer.mozilla.org/en-US/docs/Web/API/Response/json) */
606
615
  json: () => Promise<TSuccess>;
607
616
  }
608
617
 
609
618
  /** @see https://developer.mozilla.org/en-US/docs/Web/API/Response */
610
- interface ErrorResponse<TData, TStatusCode> extends Omit<Response, "ok" | "status" | "json"> {
619
+ export interface TypedErrorResponse<TData, TStatusCode, THeaders> extends Omit<Response, "ok" | "status" | "json" | "headers"> {
611
620
  ok: false;
612
621
  status: TStatusCode;
622
+ headers: never extends THeaders ? Headers : TypedHeaders<THeaders>;
613
623
  data: TData;
614
624
  /** [MDN Reference](https://developer.mozilla.org/en-US/docs/Web/API/Response/json) */
615
625
  json: () => Promise<TData>;
616
626
  }
617
627
 
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]);
628
+ export type TypedApiResponse<TAllResponses extends Record<string | number, unknown> = {}, THeaders = {}> =
629
+ ({
630
+ [K in keyof TAllResponses]: K extends string
631
+ ? K extends \`\${infer TStatusCode extends number}\`
632
+ ? TStatusCode extends SuccessStatusCode
633
+ ? TypedSuccessResponse<TAllResponses[K], TStatusCode, K extends keyof THeaders ? THeaders[K] : never>
634
+ : TypedErrorResponse<TAllResponses[K], TStatusCode, K extends keyof THeaders ? THeaders[K] : never>
635
+ : never
636
+ : K extends number
637
+ ? K extends SuccessStatusCode
638
+ ? TypedSuccessResponse<TAllResponses[K], K, K extends keyof THeaders ? THeaders[K] : never>
639
+ : TypedErrorResponse<TAllResponses[K], K, K extends keyof THeaders ? THeaders[K] : never>
640
+ : never;
641
+ }[keyof TAllResponses]);
634
642
 
635
- export type SafeApiResponse<TEndpoint> = TEndpoint extends { response: infer TSuccess; responses: infer TResponses }
643
+ export type SafeApiResponse<TEndpoint> = TEndpoint extends { responses: infer TResponses }
636
644
  ? 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;
645
+ ? TypedApiResponse<TResponses, TEndpoint extends { responseHeaders: infer THeaders } ? THeaders : never>
646
+ : never
647
+ : never
642
648
 
643
649
  export type InferResponseByStatus<TEndpoint, TStatusCode> = Extract<SafeApiResponse<TEndpoint>, { status: TStatusCode }>
644
650
 
@@ -653,9 +659,9 @@ type MaybeOptionalArg<T> = RequiredKeys<T> extends never ? [config?: T] : [confi
653
659
  const apiClient = `
654
660
  // <TypedResponseError>
655
661
  export class TypedResponseError extends Error {
656
- response: ErrorResponse<unknown, ErrorStatusCode>;
662
+ response: TypedErrorResponse<unknown, ErrorStatusCode, unknown>;
657
663
  status: number;
658
- constructor(response: ErrorResponse<unknown, ErrorStatusCode>) {
664
+ constructor(response: TypedErrorResponse<unknown, ErrorStatusCode, unknown>) {
659
665
  super(\`HTTP \${response.status}: \${response.statusText}\`);
660
666
  this.name = 'TypedResponseError';
661
667
  this.response = response;
@@ -691,7 +697,13 @@ export class ApiClient {
691
697
  ${method}<Path extends keyof ${capitalizedMethod}Endpoints, TEndpoint extends ${capitalizedMethod}Endpoints[Path]>(
692
698
  path: Path,
693
699
  ...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"]`)}>;
700
+ ): Promise<${match(ctx.runtime).with("zod", "yup", () => infer(`InferResponseByStatus<TEndpoint, SuccessStatusCode>`)).with(
701
+ "arktype",
702
+ "io-ts",
703
+ "typebox",
704
+ "valibot",
705
+ () => `InferResponseByStatus<${infer(`TEndpoint`)}, SuccessStatusCode>["data"]`
706
+ ).otherwise(() => `InferResponseByStatus<TEndpoint, SuccessStatusCode>["data"]`)}>;
695
707
 
696
708
  ${method}<Path extends keyof ${capitalizedMethod}Endpoints, TEndpoint extends ${capitalizedMethod}Endpoints[Path]>(
697
709
  path: Path,
@@ -721,7 +733,13 @@ export class ApiClient {
721
733
  return withResponse ? typedResponse : data;
722
734
  });
723
735
 
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"]>`)}
736
+ return promise ${match(ctx.runtime).with("zod", "yup", () => `as Promise<${infer(`InferResponseByStatus<TEndpoint, SuccessStatusCode>`)}>`).with(
737
+ "arktype",
738
+ "io-ts",
739
+ "typebox",
740
+ "valibot",
741
+ () => `as Promise<InferResponseByStatus<${infer(`TEndpoint`)}, SuccessStatusCode>["data"]>`
742
+ ).otherwise(() => `as Promise<InferResponseByStatus<TEndpoint, SuccessStatusCode>["data"]>`)}
725
743
  }
726
744
  // </ApiClient.${method}>
727
745
  ` : "";
@@ -1082,7 +1100,6 @@ var mapOpenApiEndpoints = (doc, options) => {
1082
1100
  method,
1083
1101
  path,
1084
1102
  requestFormat: "json",
1085
- response: openApiSchemaToTs({ schema: {}, ctx }),
1086
1103
  meta: {
1087
1104
  alias,
1088
1105
  areParametersRequired: false,
@@ -1124,11 +1141,11 @@ var mapOpenApiEndpoints = (doc, options) => {
1124
1141
  if (operation.requestBody) {
1125
1142
  endpoint.meta.hasParameters = true;
1126
1143
  const requestBody = refs.unwrap(operation.requestBody ?? {});
1127
- const content2 = requestBody.content;
1128
- const matchingMediaType = Object.keys(content2).find(isAllowedParamMediaTypes);
1129
- if (matchingMediaType && content2[matchingMediaType]) {
1144
+ const content = requestBody.content;
1145
+ const matchingMediaType = Object.keys(content).find(isAllowedParamMediaTypes);
1146
+ if (matchingMediaType && content[matchingMediaType]) {
1130
1147
  params.body = openApiSchemaToTs({
1131
- schema: content2[matchingMediaType]?.schema ?? {},
1148
+ schema: content[matchingMediaType]?.schema ?? {},
1132
1149
  ctx
1133
1150
  });
1134
1151
  }
@@ -1159,67 +1176,64 @@ var mapOpenApiEndpoints = (doc, options) => {
1159
1176
  }
1160
1177
  endpoint.parameters = Object.keys(params).length ? params : void 0;
1161
1178
  }
1162
- let responseObject;
1163
1179
  const allResponses = {};
1180
+ const allHeaders = {};
1164
1181
  Object.entries(operation.responses ?? {}).map(([status, responseOrRef]) => {
1165
- const statusCode = Number(status);
1166
1182
  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
- });
1183
+ const content = responseObj?.content;
1184
+ const mediaTypes = Object.keys(content ?? {}).filter(isResponseMediaType);
1185
+ if (content && mediaTypes.length) {
1186
+ mediaTypes.forEach((mediaType) => {
1187
+ const schema = content[mediaType] ? content[mediaType].schema ?? {} : {};
1188
+ const t2 = createBoxFactory(schema, ctx);
1189
+ const mediaTypeResponse = openApiSchemaToTs({ schema, ctx });
1190
+ if (allResponses[status]) {
1191
+ allResponses[status] = t2.union([
1192
+ ...Array.isArray(allResponses[status]) ? allResponses[status] : [allResponses[status]],
1193
+ mediaTypeResponse
1194
+ ]);
1195
+ } else {
1196
+ allResponses[status] = mediaTypeResponse;
1197
+ }
1198
+ });
1199
+ } else {
1200
+ const schema = {};
1201
+ const unknown = openApiSchemaToTs({ schema: {}, ctx });
1202
+ const t2 = createBoxFactory(schema, ctx);
1203
+ if (allResponses[status]) {
1204
+ allResponses[status] = t2.union([
1205
+ ...Array.isArray(allResponses[status]) ? allResponses[status] : [allResponses[status]],
1206
+ unknown
1207
+ ]);
1175
1208
  } else {
1176
- allResponses[status] = openApiSchemaToTs({ schema: {}, ctx });
1209
+ allResponses[status] = unknown;
1177
1210
  }
1178
- } else {
1179
- allResponses[status] = openApiSchemaToTs({ schema: {}, ctx });
1180
1211
  }
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
- }
1212
+ const headers = responseObj?.headers;
1213
+ const t = createBoxFactory(
1214
+ { type: "object", properties: headers ?? {}, required: Object.keys(headers ?? {}) },
1215
+ ctx
1216
+ );
1217
+ if (headers) {
1218
+ const mappedHeaders = Object.entries(headers).reduce(
1219
+ (acc, [name, headerOrRef]) => {
1220
+ const header = refs.unwrap(headerOrRef);
1221
+ const box = openApiSchemaToTs({ schema: header.schema ?? {}, ctx });
1222
+ acc[name] = box;
1223
+ return acc;
1224
+ },
1225
+ {}
1226
+ );
1227
+ if (Object.keys(mappedHeaders).length) {
1228
+ allHeaders[status] = t.object(mappedHeaders);
1197
1229
  }
1198
1230
  }
1199
- }
1231
+ });
1200
1232
  if (Object.keys(allResponses).length > 0) {
1201
1233
  endpoint.responses = allResponses;
1202
1234
  }
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
- );
1235
+ if (Object.keys(allHeaders).length) {
1236
+ endpoint.responseHeaders = allHeaders;
1223
1237
  }
1224
1238
  endpointList.push(endpoint);
1225
1239
  });
@@ -1233,7 +1247,7 @@ var allowedParamMediaTypes = [
1233
1247
  "*/*"
1234
1248
  ];
1235
1249
  var isAllowedParamMediaTypes = (mediaType) => mediaType.includes("application/") && mediaType.includes("json") || allowedParamMediaTypes.includes(mediaType) || mediaType.includes("text/");
1236
- var isResponseMediaType = (mediaType) => mediaType === "application/json" || mediaType === "*/*";
1250
+ var isResponseMediaType = (mediaType) => mediaType === "*/*" || mediaType.includes("application/") && mediaType.includes("json");
1237
1251
  var getAlias = ({ path, method, operation }) => sanitizeName(
1238
1252
  (method + "_" + capitalize3(operation.operationId ?? pathToVariableName(path))).replace(/-/g, "__"),
1239
1253
  "endpoint"
@@ -1325,7 +1339,7 @@ var generateTanstackQueryFile = async (ctx) => {
1325
1339
  withResponse: false as const
1326
1340
  };
1327
1341
  const res = await this.client.${method}(path, requestParams);
1328
- return res as TEndpoint["response"];
1342
+ return res as InferResponseByStatus<TEndpoint, SuccessStatusCode>["data"];
1329
1343
  },
1330
1344
  queryKey: queryKey
1331
1345
  }),
@@ -1340,7 +1354,7 @@ var generateTanstackQueryFile = async (ctx) => {
1340
1354
  withResponse: false as const
1341
1355
  };
1342
1356
  const res = await this.client.${method}(path, requestParams);
1343
- return res as TEndpoint["response"];
1357
+ return res as InferResponseByStatus<TEndpoint, SuccessStatusCode>["data"];
1344
1358
  }
1345
1359
  }
1346
1360
  };
@@ -5,7 +5,7 @@ import {
5
5
  generateFile,
6
6
  generateTanstackQueryFile,
7
7
  mapOpenApiEndpoints
8
- } from "./chunk-4EZJSCLI.js";
8
+ } from "./chunk-I2PUFY2J.js";
9
9
  import {
10
10
  prettify
11
11
  } from "./chunk-KAEXXJ7X.js";
package/dist/cli.js CHANGED
@@ -1,9 +1,9 @@
1
1
  import {
2
2
  generateClientFiles
3
- } from "./chunk-GD55PFNE.js";
3
+ } from "./chunk-J2KG2QH2.js";
4
4
  import {
5
5
  allowedRuntimes
6
- } from "./chunk-4EZJSCLI.js";
6
+ } from "./chunk-I2PUFY2J.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-4EZJSCLI.js";
11
+ } from "./chunk-I2PUFY2J.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-GD55PFNE.js";
4
- import "./chunk-4EZJSCLI.js";
3
+ } from "./chunk-J2KG2QH2.js";
4
+ import "./chunk-I2PUFY2J.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.1",
4
+ "version": "2.1.0",
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,7 +170,7 @@ 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>>, ctx: GeneratorContext) => {
174
174
  let str = "{";
175
175
  for (const [key, responseHeader] of Object.entries(responseHeaders)) {
176
176
  const value =
@@ -242,17 +242,6 @@ const generateEndpointSchemaList = (ctx: GeneratorContext) => {
242
242
  }`
243
243
  : "parameters: never,"
244
244
  }
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
245
  ${endpoint.responses ? `responses: ${generateResponsesObject(endpoint.responses, ctx)},` : ""}
257
246
  ${
258
247
  endpoint.responseHeaders
@@ -331,7 +320,6 @@ type RequestFormat = "json" | "form-data" | "form-url" | "binary" | "text";
331
320
 
332
321
  export type DefaultEndpoint = {
333
322
  parameters?: EndpointParameters | undefined;
334
- response: unknown;
335
323
  responses?: Record<string, unknown>;
336
324
  responseHeaders?: Record<string, unknown>;
337
325
  };
@@ -347,7 +335,6 @@ export type Endpoint<TConfig extends DefaultEndpoint = DefaultEndpoint> = {
347
335
  hasParameters: boolean;
348
336
  areParametersRequired: boolean;
349
337
  };
350
- response: TConfig["response"];
351
338
  responses?: TConfig["responses"];
352
339
  responseHeaders?: TConfig["responseHeaders"]
353
340
  };
@@ -360,49 +347,63 @@ export type SuccessStatusCode = typeof successStatusCodes[number];
360
347
  export const errorStatusCodes = [${ctx.errorStatusCodes.join(",")}] as const;
361
348
  export type ErrorStatusCode = typeof errorStatusCodes[number];
362
349
 
363
- // Error handling types
350
+ // Taken from https://github.com/unjs/fetchdts/blob/ec4eaeab5d287116171fc1efd61f4a1ad34e4609/src/fetch.ts#L3
351
+ export interface TypedHeaders<TypedHeaderValues extends Record<string, string> | unknown> extends Omit<Headers, 'append' | 'delete' | 'get' | 'getSetCookie' | 'has' | 'set' | 'forEach'> {
352
+ /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Headers/append) */
353
+ append: <Name extends Extract<keyof TypedHeaderValues, string> | string & {}> (name: Name, value: Lowercase<Name> extends keyof TypedHeaderValues ? TypedHeaderValues[Lowercase<Name>] : string) => void
354
+ /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Headers/delete) */
355
+ delete: <Name extends Extract<keyof TypedHeaderValues, string> | string & {}> (name: Name) => void
356
+ /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Headers/get) */
357
+ get: <Name extends Extract<keyof TypedHeaderValues, string> | string & {}> (name: Name) => (Lowercase<Name> extends keyof TypedHeaderValues ? TypedHeaderValues[Lowercase<Name>] : string) | null
358
+ /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Headers/getSetCookie) */
359
+ getSetCookie: () => string[]
360
+ /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Headers/has) */
361
+ has: <Name extends Extract<keyof TypedHeaderValues, string> | string & {}> (name: Name) => boolean
362
+ /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Headers/set) */
363
+ set: <Name extends Extract<keyof TypedHeaderValues, string> | string & {}> (name: Name, value: Lowercase<Name> extends keyof TypedHeaderValues ? TypedHeaderValues[Lowercase<Name>] : string) => void
364
+ forEach: (callbackfn: (value: TypedHeaderValues[keyof TypedHeaderValues] | string & {}, key: Extract<keyof TypedHeaderValues, string> | string & {}, parent: TypedHeaders<TypedHeaderValues>) => void, thisArg?: any) => void
365
+ }
366
+
364
367
  /** @see https://developer.mozilla.org/en-US/docs/Web/API/Response */
365
- interface SuccessResponse<TSuccess, TStatusCode> extends Omit<Response, "ok" | "status" | "json"> {
368
+ export interface TypedSuccessResponse<TSuccess, TStatusCode, THeaders> extends Omit<Response, "ok" | "status" | "json" | "headers"> {
366
369
  ok: true;
367
370
  status: TStatusCode;
371
+ headers: never extends THeaders ? Headers : TypedHeaders<THeaders>;
368
372
  data: TSuccess;
369
373
  /** [MDN Reference](https://developer.mozilla.org/en-US/docs/Web/API/Response/json) */
370
374
  json: () => Promise<TSuccess>;
371
375
  }
372
376
 
373
377
  /** @see https://developer.mozilla.org/en-US/docs/Web/API/Response */
374
- interface ErrorResponse<TData, TStatusCode> extends Omit<Response, "ok" | "status" | "json"> {
378
+ export interface TypedErrorResponse<TData, TStatusCode, THeaders> extends Omit<Response, "ok" | "status" | "json" | "headers"> {
375
379
  ok: false;
376
380
  status: TStatusCode;
381
+ headers: never extends THeaders ? Headers : TypedHeaders<THeaders>;
377
382
  data: TData;
378
383
  /** [MDN Reference](https://developer.mozilla.org/en-US/docs/Web/API/Response/json) */
379
384
  json: () => Promise<TData>;
380
385
  }
381
386
 
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 }
387
+ export type TypedApiResponse<TAllResponses extends Record<string | number, unknown> = {}, THeaders = {}> =
388
+ ({
389
+ [K in keyof TAllResponses]: K extends string
390
+ ? K extends \`\${infer TStatusCode extends number}\`
391
+ ? TStatusCode extends SuccessStatusCode
392
+ ? TypedSuccessResponse<TAllResponses[K], TStatusCode, K extends keyof THeaders ? THeaders[K] : never>
393
+ : TypedErrorResponse<TAllResponses[K], TStatusCode, K extends keyof THeaders ? THeaders[K] : never>
394
+ : never
395
+ : K extends number
396
+ ? K extends SuccessStatusCode
397
+ ? TypedSuccessResponse<TAllResponses[K], K, K extends keyof THeaders ? THeaders[K] : never>
398
+ : TypedErrorResponse<TAllResponses[K], K, K extends keyof THeaders ? THeaders[K] : never>
399
+ : never;
400
+ }[keyof TAllResponses]);
401
+
402
+ export type SafeApiResponse<TEndpoint> = TEndpoint extends { responses: infer TResponses }
400
403
  ? 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;
404
+ ? TypedApiResponse<TResponses, TEndpoint extends { responseHeaders: infer THeaders } ? THeaders : never>
405
+ : never
406
+ : never
406
407
 
407
408
  export type InferResponseByStatus<TEndpoint, TStatusCode> = Extract<SafeApiResponse<TEndpoint>, { status: TStatusCode }>
408
409
 
@@ -418,9 +419,9 @@ type MaybeOptionalArg<T> = RequiredKeys<T> extends never ? [config?: T] : [confi
418
419
  const apiClient = `
419
420
  // <TypedResponseError>
420
421
  export class TypedResponseError extends Error {
421
- response: ErrorResponse<unknown, ErrorStatusCode>;
422
+ response: TypedErrorResponse<unknown, ErrorStatusCode, unknown>;
422
423
  status: number;
423
- constructor(response: ErrorResponse<unknown, ErrorStatusCode>) {
424
+ constructor(response: TypedErrorResponse<unknown, ErrorStatusCode, unknown>) {
424
425
  super(\`HTTP \${response.status}: \${response.statusText}\`);
425
426
  this.name = 'TypedResponseError';
426
427
  this.response = response;
@@ -463,9 +464,15 @@ export class ApiClient {
463
464
  .with("arktype", "io-ts", "typebox", "valibot", () => infer(`TEndpoint`) + `["parameters"]`)
464
465
  .otherwise(() => `TEndpoint["parameters"]`)} & { withResponse?: false; throwOnStatusError?: boolean }>
465
466
  ): 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"]`)}>;
467
+ .with("zod", "yup", () => infer(`InferResponseByStatus<TEndpoint, SuccessStatusCode>`))
468
+ .with(
469
+ "arktype",
470
+ "io-ts",
471
+ "typebox",
472
+ "valibot",
473
+ () => `InferResponseByStatus<${infer(`TEndpoint`)}, SuccessStatusCode>["data"]`,
474
+ )
475
+ .otherwise(() => `InferResponseByStatus<TEndpoint, SuccessStatusCode>["data"]`)}>;
469
476
 
470
477
  ${method}<Path extends keyof ${capitalizedMethod}Endpoints, TEndpoint extends ${capitalizedMethod}Endpoints[Path]>(
471
478
  path: Path,
@@ -499,9 +506,15 @@ export class ApiClient {
499
506
  });
500
507
 
501
508
  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"]>`)}
509
+ .with("zod", "yup", () => `as Promise<${infer(`InferResponseByStatus<TEndpoint, SuccessStatusCode>`)}>`)
510
+ .with(
511
+ "arktype",
512
+ "io-ts",
513
+ "typebox",
514
+ "valibot",
515
+ () => `as Promise<InferResponseByStatus<${infer(`TEndpoint`)}, SuccessStatusCode>["data"]>`,
516
+ )
517
+ .otherwise(() => `as Promise<InferResponseByStatus<TEndpoint, SuccessStatusCode>["data"]>`)}
505
518
  }
506
519
  // </ApiClient.${method}>
507
520
  `
@@ -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 === "application/json" || mediaType === "*/*";
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
  };