openapi-typesafe-fetch 1.0.0-rc.10 → 1.0.0-rc.11

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.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,27 @@
2
2
 
3
3
  This package is a fork of `openapi-fetch`. The entries above `0.17.0` are releases of openapi-typesafe-fetch; `0.17.0` and the entries below it are the upstream history up to the fork point (upstream commit `d46a319a`), with `openapi-fetch` versions.
4
4
 
5
+ ## 1.0.0-rc.11
6
+
7
+ ### Patch Changes
8
+
9
+ - [#128](https://github.com/openapi-typesafe/openapi-typesafe/pull/128) [`fad3163`](https://github.com/openapi-typesafe/openapi-typesafe/commit/fad31635ed28508082d59c2e0a3ba6e881b20d02) Thanks [@fedorello](https://github.com/fedorello)! - A `json` inside the parameters of a media type no longer makes it a JSON media type in the types,
10
+ as at runtime: an error declared as `text/plain; profile=urn:a+json` or
11
+ `message/http; msgtype=application/json` is typed as the text (`string`) the client returns, and
12
+ `JSONLike`, `RequestBodyJSON`, `SuccessResponseJSON` and `ErrorResponseJSON` leave such media
13
+ types out. Only the media type before the `;` counts, for media ranges (`text/*`) too.
14
+
15
+ - [#128](https://github.com/openapi-typesafe/openapi-typesafe/pull/128) [`fad3163`](https://github.com/openapi-typesafe/openapi-typesafe/commit/fad31635ed28508082d59c2e0a3ba6e881b20d02) Thanks [@fedorello](https://github.com/fedorello)! - An optional `parseAs` with only some values, such as a wrapper's `options: { parseAs?: "text" }`
16
+ passed to `client.GET()`, types `data` as the body of each value or the JSON body (`string | T`),
17
+ as the client returns it. Before, it was typed as the JSON body only. Options whose `parseAs` may
18
+ be any value, or that have none, still type `data` as the JSON body. With and without
19
+ `exactOptionalPropertyTypes`.
20
+
21
+ - [#128](https://github.com/openapi-typesafe/openapi-typesafe/pull/128) [`fad3163`](https://github.com/openapi-typesafe/openapi-typesafe/commit/fad31635ed28508082d59c2e0a3ba6e881b20d02) Thanks [@fedorello](https://github.com/fedorello)! - `client.request(method, path, { parseAs })` types `data` by `parseAs`, as `client.GET()` and the
22
+ other method calls do: `{ parseAs: "text" }` gives a `string`, `"stream"` the body stream. Before,
23
+ `data` was always the JSON body. A literal in `params` or `body` next to `parseAs`, such as an
24
+ enum value, no longer loses the `parseAs` type either.
25
+
5
26
  ## 1.0.0-rc.10
6
27
 
7
28
  ### Patch Changes
@@ -272,12 +272,26 @@ type JSONErrorMediaTypes<Content> = {
272
272
  type TextErrorMediaTypes<Content> = {
273
273
  [Key in keyof Content]: IsJSONMediaPattern<Key> extends true ? never : Key;
274
274
  }[keyof Content];
275
- /** Whether the client parses every body of the media type `Key` as JSON, in any letter case. */
276
- type IsJSONMediaPattern<Key> = Key extends string ? Lowercase<Key> extends JSONMediaPattern ? true : false : false;
275
+ /**
276
+ * Whether the client parses every body of the media type `Key` as JSON, in any letter case: its
277
+ * subtype is `json` or ends in `+json`, as `JSON_MEDIA_TYPE_RE` reads it. A `json` in a parameter
278
+ * (`text/plain; profile=urn:a+json`) does not count; `JSONMediaPattern` alone matched it.
279
+ */
280
+ type IsJSONMediaPattern<Key> = Key extends string ? MediaSubtype<Lowercase<Key>> extends "json" | `${string}+json` ? true : false : false;
281
+ /**
282
+ * The subtype of a media type: the text after the first `/` and before the parameters (the first
283
+ * `;`), without the spaces and tabs at its end. Empty without a type before a `/`, as
284
+ * `JSON_MEDIA_TYPE_RE` wants one. A subtype may hold a `/`, as the regular expression allows.
285
+ */
286
+ type MediaSubtype<Text extends string> = Text extends `${infer Type}/${infer Rest}` ? Type extends "" ? "" : TrimmedEnd<Rest extends `${infer Subtype};${string}` ? Subtype : Rest> : "";
287
+ /** The text without the spaces and tabs at its end (OWS before parameters, RFC 9110). */
288
+ type TrimmedEnd<Text extends string> = Text extends `${infer Rest} ` | `${infer Rest}\t` ? TrimmedEnd<Rest> : Text;
277
289
  /**
278
290
  * The media types that the client parses as JSON, as a content map writes them: `json` or a
279
291
  * `+json` suffix (RFC 6838), with or without parameters, as the client's `JSON_MEDIA_TYPE_RE`.
280
- * Not `x-ndjson` or `json-seq`, whose bodies are lines of JSON.
292
+ * Not `x-ndjson` or `json-seq`, whose bodies are lines of JSON. A template cannot keep its
293
+ * `${string}` out of the parameters, so it only picks the schema of an index signature
294
+ * (`JSONMediaKey`); whether a media type is JSON is `IsJSONMediaPattern`'s answer.
281
295
  */
282
296
  type JSONMediaPattern = `${string}/${"" | `${string}+`}json` | `${string}/${"" | `${string}+`}json${";" | " " | "\t"}${string}`;
283
297
  /**
@@ -287,12 +301,31 @@ type JSONMediaPattern = `${string}/${"" | `${string}+`}json` | `${string}/${"" |
287
301
  type JSONMediaRange = `${string}/*` | `${string}/*${";" | " " | "\t"}${string}`;
288
302
  /** The media types that may hold JSON, in lower case. */
289
303
  type JSONMediaType = JSONMediaPattern | JSONMediaRange;
290
- /** Whether the client parses a body of the media type `Key` as JSON, in any letter case. */
291
- type IsJSONMediaType<Key> = Key extends string ? Lowercase<Key> extends JSONMediaType ? true : false : false;
304
+ /**
305
+ * Whether a body of the media type `Key` may be one the client parses as JSON, in any letter
306
+ * case: a JSON media type (`IsJSONMediaPattern`) or a media range (subtype `*`), with or without
307
+ * parameters, which do not count.
308
+ */
309
+ type IsJSONMediaType<Key> = Key extends string ? IsJSONMediaPattern<Key> extends true ? true : MediaSubtype<Lowercase<Key>> extends "*" ? true : false : false;
292
310
  /** Return first JSON-like 2XX response from a path + HTTP method */
293
- type SuccessResponseJSON<PathMethod extends Record<string | number, any>> = SuccessResponse<ResponseObjectMap<PathMethod>, JSONMediaType>;
311
+ type SuccessResponseJSON<PathMethod extends Record<string | number, any>> = SuccessResponse<ResponseObjectMap<PathMethod>, JSONMediaKeys<ResponseObjectMap<PathMethod>>>;
294
312
  /** Return JSON-like 1XX, 3XX, 4XX, 5XX and `default` responses from a path + HTTP method */
295
- type ErrorResponseJSON<PathMethod extends Record<string | number, any>> = ErrorResponse<ResponseObjectMap<PathMethod>, JSONMediaType>;
313
+ type ErrorResponseJSON<PathMethod extends Record<string | number, any>> = ErrorResponse<ResponseObjectMap<PathMethod>, JSONMediaKeys<ResponseObjectMap<PathMethod>>>;
314
+ /**
315
+ * The media types of the responses `T` that may hold JSON (`IsJSONMediaType`), for the JSON
316
+ * helpers to pick by: their keys, as `JSONMediaType` cannot leave out a `json` in parameters. A
317
+ * content map with an index signature has no such key, so it gives `JSONMediaType`, which picks
318
+ * the schema of its signature.
319
+ */
320
+ type JSONMediaKeys<T> = {
321
+ [Status in keyof T]: T[Status] extends {
322
+ content: infer Content;
323
+ } ? {
324
+ [Key in keyof Content]: JSONMediaKey<Key>;
325
+ }[keyof Content] : never;
326
+ }[keyof T];
327
+ /** `Key` when it may hold JSON; `JSONMediaType` for the wide key of an index signature. */
328
+ type JSONMediaKey<Key> = string extends Key ? JSONMediaType : IsJSONMediaType<Key> extends true ? Key : never;
296
329
  /** Return JSON-like request body from a path + HTTP method */
297
330
  type RequestBodyJSON<PathMethod> = JSONLike<FilterKeys<OperationRequestBody<PathMethod>, "content">>;
298
331
  /** Find first match of multiple keys */
@@ -272,12 +272,26 @@ type JSONErrorMediaTypes<Content> = {
272
272
  type TextErrorMediaTypes<Content> = {
273
273
  [Key in keyof Content]: IsJSONMediaPattern<Key> extends true ? never : Key;
274
274
  }[keyof Content];
275
- /** Whether the client parses every body of the media type `Key` as JSON, in any letter case. */
276
- type IsJSONMediaPattern<Key> = Key extends string ? Lowercase<Key> extends JSONMediaPattern ? true : false : false;
275
+ /**
276
+ * Whether the client parses every body of the media type `Key` as JSON, in any letter case: its
277
+ * subtype is `json` or ends in `+json`, as `JSON_MEDIA_TYPE_RE` reads it. A `json` in a parameter
278
+ * (`text/plain; profile=urn:a+json`) does not count; `JSONMediaPattern` alone matched it.
279
+ */
280
+ type IsJSONMediaPattern<Key> = Key extends string ? MediaSubtype<Lowercase<Key>> extends "json" | `${string}+json` ? true : false : false;
281
+ /**
282
+ * The subtype of a media type: the text after the first `/` and before the parameters (the first
283
+ * `;`), without the spaces and tabs at its end. Empty without a type before a `/`, as
284
+ * `JSON_MEDIA_TYPE_RE` wants one. A subtype may hold a `/`, as the regular expression allows.
285
+ */
286
+ type MediaSubtype<Text extends string> = Text extends `${infer Type}/${infer Rest}` ? Type extends "" ? "" : TrimmedEnd<Rest extends `${infer Subtype};${string}` ? Subtype : Rest> : "";
287
+ /** The text without the spaces and tabs at its end (OWS before parameters, RFC 9110). */
288
+ type TrimmedEnd<Text extends string> = Text extends `${infer Rest} ` | `${infer Rest}\t` ? TrimmedEnd<Rest> : Text;
277
289
  /**
278
290
  * The media types that the client parses as JSON, as a content map writes them: `json` or a
279
291
  * `+json` suffix (RFC 6838), with or without parameters, as the client's `JSON_MEDIA_TYPE_RE`.
280
- * Not `x-ndjson` or `json-seq`, whose bodies are lines of JSON.
292
+ * Not `x-ndjson` or `json-seq`, whose bodies are lines of JSON. A template cannot keep its
293
+ * `${string}` out of the parameters, so it only picks the schema of an index signature
294
+ * (`JSONMediaKey`); whether a media type is JSON is `IsJSONMediaPattern`'s answer.
281
295
  */
282
296
  type JSONMediaPattern = `${string}/${"" | `${string}+`}json` | `${string}/${"" | `${string}+`}json${";" | " " | "\t"}${string}`;
283
297
  /**
@@ -287,12 +301,31 @@ type JSONMediaPattern = `${string}/${"" | `${string}+`}json` | `${string}/${"" |
287
301
  type JSONMediaRange = `${string}/*` | `${string}/*${";" | " " | "\t"}${string}`;
288
302
  /** The media types that may hold JSON, in lower case. */
289
303
  type JSONMediaType = JSONMediaPattern | JSONMediaRange;
290
- /** Whether the client parses a body of the media type `Key` as JSON, in any letter case. */
291
- type IsJSONMediaType<Key> = Key extends string ? Lowercase<Key> extends JSONMediaType ? true : false : false;
304
+ /**
305
+ * Whether a body of the media type `Key` may be one the client parses as JSON, in any letter
306
+ * case: a JSON media type (`IsJSONMediaPattern`) or a media range (subtype `*`), with or without
307
+ * parameters, which do not count.
308
+ */
309
+ type IsJSONMediaType<Key> = Key extends string ? IsJSONMediaPattern<Key> extends true ? true : MediaSubtype<Lowercase<Key>> extends "*" ? true : false : false;
292
310
  /** Return first JSON-like 2XX response from a path + HTTP method */
293
- type SuccessResponseJSON<PathMethod extends Record<string | number, any>> = SuccessResponse<ResponseObjectMap<PathMethod>, JSONMediaType>;
311
+ type SuccessResponseJSON<PathMethod extends Record<string | number, any>> = SuccessResponse<ResponseObjectMap<PathMethod>, JSONMediaKeys<ResponseObjectMap<PathMethod>>>;
294
312
  /** Return JSON-like 1XX, 3XX, 4XX, 5XX and `default` responses from a path + HTTP method */
295
- type ErrorResponseJSON<PathMethod extends Record<string | number, any>> = ErrorResponse<ResponseObjectMap<PathMethod>, JSONMediaType>;
313
+ type ErrorResponseJSON<PathMethod extends Record<string | number, any>> = ErrorResponse<ResponseObjectMap<PathMethod>, JSONMediaKeys<ResponseObjectMap<PathMethod>>>;
314
+ /**
315
+ * The media types of the responses `T` that may hold JSON (`IsJSONMediaType`), for the JSON
316
+ * helpers to pick by: their keys, as `JSONMediaType` cannot leave out a `json` in parameters. A
317
+ * content map with an index signature has no such key, so it gives `JSONMediaType`, which picks
318
+ * the schema of its signature.
319
+ */
320
+ type JSONMediaKeys<T> = {
321
+ [Status in keyof T]: T[Status] extends {
322
+ content: infer Content;
323
+ } ? {
324
+ [Key in keyof Content]: JSONMediaKey<Key>;
325
+ }[keyof Content] : never;
326
+ }[keyof T];
327
+ /** `Key` when it may hold JSON; `JSONMediaType` for the wide key of an index signature. */
328
+ type JSONMediaKey<Key> = string extends Key ? JSONMediaType : IsJSONMediaType<Key> extends true ? Key : never;
296
329
  /** Return JSON-like request body from a path + HTTP method */
297
330
  type RequestBodyJSON<PathMethod> = JSONLike<FilterKeys<OperationRequestBody<PathMethod>, "content">>;
298
331
  /** Find first match of multiple keys */
package/dist/helpers.d.ts CHANGED
@@ -272,12 +272,26 @@ type JSONErrorMediaTypes<Content> = {
272
272
  type TextErrorMediaTypes<Content> = {
273
273
  [Key in keyof Content]: IsJSONMediaPattern<Key> extends true ? never : Key;
274
274
  }[keyof Content];
275
- /** Whether the client parses every body of the media type `Key` as JSON, in any letter case. */
276
- type IsJSONMediaPattern<Key> = Key extends string ? Lowercase<Key> extends JSONMediaPattern ? true : false : false;
275
+ /**
276
+ * Whether the client parses every body of the media type `Key` as JSON, in any letter case: its
277
+ * subtype is `json` or ends in `+json`, as `JSON_MEDIA_TYPE_RE` reads it. A `json` in a parameter
278
+ * (`text/plain; profile=urn:a+json`) does not count; `JSONMediaPattern` alone matched it.
279
+ */
280
+ type IsJSONMediaPattern<Key> = Key extends string ? MediaSubtype<Lowercase<Key>> extends "json" | `${string}+json` ? true : false : false;
281
+ /**
282
+ * The subtype of a media type: the text after the first `/` and before the parameters (the first
283
+ * `;`), without the spaces and tabs at its end. Empty without a type before a `/`, as
284
+ * `JSON_MEDIA_TYPE_RE` wants one. A subtype may hold a `/`, as the regular expression allows.
285
+ */
286
+ type MediaSubtype<Text extends string> = Text extends `${infer Type}/${infer Rest}` ? Type extends "" ? "" : TrimmedEnd<Rest extends `${infer Subtype};${string}` ? Subtype : Rest> : "";
287
+ /** The text without the spaces and tabs at its end (OWS before parameters, RFC 9110). */
288
+ type TrimmedEnd<Text extends string> = Text extends `${infer Rest} ` | `${infer Rest}\t` ? TrimmedEnd<Rest> : Text;
277
289
  /**
278
290
  * The media types that the client parses as JSON, as a content map writes them: `json` or a
279
291
  * `+json` suffix (RFC 6838), with or without parameters, as the client's `JSON_MEDIA_TYPE_RE`.
280
- * Not `x-ndjson` or `json-seq`, whose bodies are lines of JSON.
292
+ * Not `x-ndjson` or `json-seq`, whose bodies are lines of JSON. A template cannot keep its
293
+ * `${string}` out of the parameters, so it only picks the schema of an index signature
294
+ * (`JSONMediaKey`); whether a media type is JSON is `IsJSONMediaPattern`'s answer.
281
295
  */
282
296
  type JSONMediaPattern = `${string}/${"" | `${string}+`}json` | `${string}/${"" | `${string}+`}json${";" | " " | "\t"}${string}`;
283
297
  /**
@@ -287,12 +301,31 @@ type JSONMediaPattern = `${string}/${"" | `${string}+`}json` | `${string}/${"" |
287
301
  type JSONMediaRange = `${string}/*` | `${string}/*${";" | " " | "\t"}${string}`;
288
302
  /** The media types that may hold JSON, in lower case. */
289
303
  type JSONMediaType = JSONMediaPattern | JSONMediaRange;
290
- /** Whether the client parses a body of the media type `Key` as JSON, in any letter case. */
291
- type IsJSONMediaType<Key> = Key extends string ? Lowercase<Key> extends JSONMediaType ? true : false : false;
304
+ /**
305
+ * Whether a body of the media type `Key` may be one the client parses as JSON, in any letter
306
+ * case: a JSON media type (`IsJSONMediaPattern`) or a media range (subtype `*`), with or without
307
+ * parameters, which do not count.
308
+ */
309
+ type IsJSONMediaType<Key> = Key extends string ? IsJSONMediaPattern<Key> extends true ? true : MediaSubtype<Lowercase<Key>> extends "*" ? true : false : false;
292
310
  /** Return first JSON-like 2XX response from a path + HTTP method */
293
- type SuccessResponseJSON<PathMethod extends Record<string | number, any>> = SuccessResponse<ResponseObjectMap<PathMethod>, JSONMediaType>;
311
+ type SuccessResponseJSON<PathMethod extends Record<string | number, any>> = SuccessResponse<ResponseObjectMap<PathMethod>, JSONMediaKeys<ResponseObjectMap<PathMethod>>>;
294
312
  /** Return JSON-like 1XX, 3XX, 4XX, 5XX and `default` responses from a path + HTTP method */
295
- type ErrorResponseJSON<PathMethod extends Record<string | number, any>> = ErrorResponse<ResponseObjectMap<PathMethod>, JSONMediaType>;
313
+ type ErrorResponseJSON<PathMethod extends Record<string | number, any>> = ErrorResponse<ResponseObjectMap<PathMethod>, JSONMediaKeys<ResponseObjectMap<PathMethod>>>;
314
+ /**
315
+ * The media types of the responses `T` that may hold JSON (`IsJSONMediaType`), for the JSON
316
+ * helpers to pick by: their keys, as `JSONMediaType` cannot leave out a `json` in parameters. A
317
+ * content map with an index signature has no such key, so it gives `JSONMediaType`, which picks
318
+ * the schema of its signature.
319
+ */
320
+ type JSONMediaKeys<T> = {
321
+ [Status in keyof T]: T[Status] extends {
322
+ content: infer Content;
323
+ } ? {
324
+ [Key in keyof Content]: JSONMediaKey<Key>;
325
+ }[keyof Content] : never;
326
+ }[keyof T];
327
+ /** `Key` when it may hold JSON; `JSONMediaType` for the wide key of an index signature. */
328
+ type JSONMediaKey<Key> = string extends Key ? JSONMediaType : IsJSONMediaType<Key> extends true ? Key : never;
296
329
  /** Return JSON-like request body from a path + HTTP method */
297
330
  type RequestBodyJSON<PathMethod> = JSONLike<FilterKeys<OperationRequestBody<PathMethod>, "content">>;
298
331
  /** Find first match of multiple keys */
package/dist/index.d.cts CHANGED
@@ -129,16 +129,58 @@ type BodyType<T = unknown> = {
129
129
  };
130
130
  type ParseAs = keyof BodyType;
131
131
  /**
132
- * The data of a success body `T` read with the `parseAs` of `Options`. Each value of a union gives
133
- * its body, and `undefined` is the default `"json"`, as at runtime: a wrapper that passes on
134
- * `parseAs: "text" | undefined` gets `string | T`, not `T` only. An optional `parseAs` (the
135
- * declared init type, as for a call without options) gives `T`, as in openapi-fetch. No `infer`:
136
- * TypeScript relates a type to a deferred conditional type through both of its branches only
137
- * when it infers nothing, and generic hooks return the data of a call as this type.
132
+ * The data of a success body `T` read with the `parseAs` of `Options`. Each value it may have gives
133
+ * its body, and `undefined` or a left-out `parseAs` is the default `"json"`, as at runtime: a
134
+ * wrapper that passes on `parseAs?: "text"` gets `string | T`, not `T` only. Options without
135
+ * `parseAs`, or whose `parseAs` may be any value (the declared init type, as for a call without
136
+ * options), give `T`, as in openapi-fetch. No `infer`: TypeScript relates a type to a deferred
137
+ * conditional type through both of its branches only when it infers nothing, and generic hooks
138
+ * return the data of a call as this type.
138
139
  */
139
- type ParseAsResponse<T, Options> = Options extends { parseAs: ParseAs | undefined }
140
- ? BodyType<T>[ParseAsValue<Options["parseAs"]>]
141
- : T;
140
+ type ParseAsResponse<T, Options> = [MissingParseAsValues<Options>] extends [never]
141
+ ? T
142
+ : BodyType<T>[ParseAsValue<ParseAsValues<Options>>];
143
+
144
+ /**
145
+ * The `parseAs` values that `Options` may have; `undefined` when it may be left out. One
146
+ * conditional type, not a union: `BodyType<T>` indexed by a union of generic keys is read as the
147
+ * intersection of their bodies when another type is checked against it. `any` options give `any`,
148
+ * so their data is `any`, as in openapi-fetch: a caller that passes an `any` init (as the generic
149
+ * hooks of openapi-typesafe-react-query do) returns it as the data type it declares. The branch
150
+ * is `any` itself, not `Options`: a type parameter there stops TypeScript relating the data of a
151
+ * generic call to a wrapper's (ADR-030).
152
+ */
153
+ type ParseAsValues<Options> = Options extends unknown
154
+ ? IsAny<Options> extends true
155
+ ? // biome-ignore lint/suspicious/noExplicitAny: any options give any data, as in openapi-fetch
156
+ any
157
+ :
158
+ | (Options extends RequiredParseAs ? never : undefined)
159
+ | { [Value in ParseAs]: Options extends WithoutParseAs<Value> ? never : Value }[ParseAs]
160
+ : never;
161
+
162
+ /**
163
+ * The `parseAs` values that `Options` cannot have, as `ParseAsValues` reads them. It does not
164
+ * distribute: a union of options cannot have a value only when none of its members can.
165
+ */
166
+ type MissingParseAsValues<Options> =
167
+ | ([Options] extends [RequiredParseAs] ? undefined : never)
168
+ | { [Value in ParseAs]: [Options] extends [WithoutParseAs<Value>] ? Value : never }[ParseAs];
169
+
170
+ /** Options that always have a `parseAs`. */
171
+ type RequiredParseAs = { parseAs: ParseAs };
172
+
173
+ /**
174
+ * Options that cannot have the `parseAs` value `Value`: `undefined` options, or options whose
175
+ * `parseAs` (if any) leaves it out. Each
176
+ * check takes `Options` as its checked type and a fixed type here, so TypeScript relates the
177
+ * data of a generic call to a wrapper's (ADR-030); one union of these types instead would let
178
+ * TypeScript split `parseAs?: ParseAs` into its values and match each one. The `object` keeps
179
+ * options without `parseAs` (`{ headers }`) from failing as a weak type.
180
+ */
181
+ type WithoutParseAs<Value extends ParseAs> =
182
+ | undefined
183
+ | (object & { parseAs?: Exclude<ParseAs, Value> | undefined });
142
184
 
143
185
  /** How a `parseAs` value reads the body, for each member of a union; `undefined` reads JSON. */
144
186
  type ParseAsValue<Value> = Value extends ParseAs ? Value : "json";
@@ -178,10 +220,16 @@ type RequestBodyOption<T> =
178
220
  * gave a conditional type no `FetchOptions` argument was assignable to (TS2345). The operations
179
221
  * of a union are not merged into one deep init type, which gave TS2589 on hundreds of paths
180
222
  * (upstream #1882).
223
+ * Only the options that depend on the operation distribute; the others stay outside the
224
+ * conditional type. While `request()` has not fixed its method, TypeScript reads the init's
225
+ * contextual type through the constraints of the method and the path, and stops after ten nested
226
+ * instantiations of one conditional type: with every option inside it, `parseAs: "text"` had no
227
+ * contextual type, widened to `string`, and the init fell back to its constraint (`data` the JSON
228
+ * body). `FetchOptions<never>` is still `never`.
181
229
  */
182
- type FetchOptions<T> = T extends unknown
183
- ? RequestOptions<T> & Omit<RequestInit, "body" | "headers">
184
- : never;
230
+ type FetchOptions<T> = (T extends unknown ? OperationRequestOptions<T> : never) &
231
+ SharedRequestOptions &
232
+ Omit<RequestInit, "body" | "headers">;
185
233
 
186
234
  // Readable<T> strips $Write markers (writeOnly properties excluded from response)
187
235
  type FetchResponse<
@@ -201,18 +249,25 @@ type FetchResponse<
201
249
  response: Response;
202
250
  };
203
251
 
204
- type RequestOptions<T> = ParamsOption<T> &
252
+ type RequestOptions<T> = OperationRequestOptions<T> & SharedRequestOptions;
253
+
254
+ /** The options of a call that depend on its operation `T`. */
255
+ type OperationRequestOptions<T> = ParamsOption<T> &
205
256
  RequestBodyOption<T> & {
206
- baseUrl?: string | undefined;
207
257
  querySerializer?: QuerySerializer<T> | QuerySerializerOptions | undefined;
208
258
  bodySerializer?: BodySerializer<T> | undefined;
209
- pathSerializer?: PathSerializer | undefined;
210
- parseAs?: ParseAs | undefined;
211
- fetch?: ClientOptions["fetch"];
212
- headers?: HeadersOptions | undefined;
213
- middleware?: Middleware[] | undefined;
214
259
  };
215
260
 
261
+ /** The options of a call that are the same for every operation. */
262
+ type SharedRequestOptions = {
263
+ baseUrl?: string | undefined;
264
+ pathSerializer?: PathSerializer | undefined;
265
+ parseAs?: ParseAs | undefined;
266
+ fetch?: ClientOptions["fetch"];
267
+ headers?: HeadersOptions | undefined;
268
+ middleware?: Middleware[] | undefined;
269
+ };
270
+
216
271
  type MergedOptions<T = unknown> = {
217
272
  baseUrl: string;
218
273
  parseAs: ParseAs;
package/dist/index.d.mts CHANGED
@@ -129,16 +129,58 @@ type BodyType<T = unknown> = {
129
129
  };
130
130
  type ParseAs = keyof BodyType;
131
131
  /**
132
- * The data of a success body `T` read with the `parseAs` of `Options`. Each value of a union gives
133
- * its body, and `undefined` is the default `"json"`, as at runtime: a wrapper that passes on
134
- * `parseAs: "text" | undefined` gets `string | T`, not `T` only. An optional `parseAs` (the
135
- * declared init type, as for a call without options) gives `T`, as in openapi-fetch. No `infer`:
136
- * TypeScript relates a type to a deferred conditional type through both of its branches only
137
- * when it infers nothing, and generic hooks return the data of a call as this type.
132
+ * The data of a success body `T` read with the `parseAs` of `Options`. Each value it may have gives
133
+ * its body, and `undefined` or a left-out `parseAs` is the default `"json"`, as at runtime: a
134
+ * wrapper that passes on `parseAs?: "text"` gets `string | T`, not `T` only. Options without
135
+ * `parseAs`, or whose `parseAs` may be any value (the declared init type, as for a call without
136
+ * options), give `T`, as in openapi-fetch. No `infer`: TypeScript relates a type to a deferred
137
+ * conditional type through both of its branches only when it infers nothing, and generic hooks
138
+ * return the data of a call as this type.
138
139
  */
139
- type ParseAsResponse<T, Options> = Options extends { parseAs: ParseAs | undefined }
140
- ? BodyType<T>[ParseAsValue<Options["parseAs"]>]
141
- : T;
140
+ type ParseAsResponse<T, Options> = [MissingParseAsValues<Options>] extends [never]
141
+ ? T
142
+ : BodyType<T>[ParseAsValue<ParseAsValues<Options>>];
143
+
144
+ /**
145
+ * The `parseAs` values that `Options` may have; `undefined` when it may be left out. One
146
+ * conditional type, not a union: `BodyType<T>` indexed by a union of generic keys is read as the
147
+ * intersection of their bodies when another type is checked against it. `any` options give `any`,
148
+ * so their data is `any`, as in openapi-fetch: a caller that passes an `any` init (as the generic
149
+ * hooks of openapi-typesafe-react-query do) returns it as the data type it declares. The branch
150
+ * is `any` itself, not `Options`: a type parameter there stops TypeScript relating the data of a
151
+ * generic call to a wrapper's (ADR-030).
152
+ */
153
+ type ParseAsValues<Options> = Options extends unknown
154
+ ? IsAny<Options> extends true
155
+ ? // biome-ignore lint/suspicious/noExplicitAny: any options give any data, as in openapi-fetch
156
+ any
157
+ :
158
+ | (Options extends RequiredParseAs ? never : undefined)
159
+ | { [Value in ParseAs]: Options extends WithoutParseAs<Value> ? never : Value }[ParseAs]
160
+ : never;
161
+
162
+ /**
163
+ * The `parseAs` values that `Options` cannot have, as `ParseAsValues` reads them. It does not
164
+ * distribute: a union of options cannot have a value only when none of its members can.
165
+ */
166
+ type MissingParseAsValues<Options> =
167
+ | ([Options] extends [RequiredParseAs] ? undefined : never)
168
+ | { [Value in ParseAs]: [Options] extends [WithoutParseAs<Value>] ? Value : never }[ParseAs];
169
+
170
+ /** Options that always have a `parseAs`. */
171
+ type RequiredParseAs = { parseAs: ParseAs };
172
+
173
+ /**
174
+ * Options that cannot have the `parseAs` value `Value`: `undefined` options, or options whose
175
+ * `parseAs` (if any) leaves it out. Each
176
+ * check takes `Options` as its checked type and a fixed type here, so TypeScript relates the
177
+ * data of a generic call to a wrapper's (ADR-030); one union of these types instead would let
178
+ * TypeScript split `parseAs?: ParseAs` into its values and match each one. The `object` keeps
179
+ * options without `parseAs` (`{ headers }`) from failing as a weak type.
180
+ */
181
+ type WithoutParseAs<Value extends ParseAs> =
182
+ | undefined
183
+ | (object & { parseAs?: Exclude<ParseAs, Value> | undefined });
142
184
 
143
185
  /** How a `parseAs` value reads the body, for each member of a union; `undefined` reads JSON. */
144
186
  type ParseAsValue<Value> = Value extends ParseAs ? Value : "json";
@@ -178,10 +220,16 @@ type RequestBodyOption<T> =
178
220
  * gave a conditional type no `FetchOptions` argument was assignable to (TS2345). The operations
179
221
  * of a union are not merged into one deep init type, which gave TS2589 on hundreds of paths
180
222
  * (upstream #1882).
223
+ * Only the options that depend on the operation distribute; the others stay outside the
224
+ * conditional type. While `request()` has not fixed its method, TypeScript reads the init's
225
+ * contextual type through the constraints of the method and the path, and stops after ten nested
226
+ * instantiations of one conditional type: with every option inside it, `parseAs: "text"` had no
227
+ * contextual type, widened to `string`, and the init fell back to its constraint (`data` the JSON
228
+ * body). `FetchOptions<never>` is still `never`.
181
229
  */
182
- type FetchOptions<T> = T extends unknown
183
- ? RequestOptions<T> & Omit<RequestInit, "body" | "headers">
184
- : never;
230
+ type FetchOptions<T> = (T extends unknown ? OperationRequestOptions<T> : never) &
231
+ SharedRequestOptions &
232
+ Omit<RequestInit, "body" | "headers">;
185
233
 
186
234
  // Readable<T> strips $Write markers (writeOnly properties excluded from response)
187
235
  type FetchResponse<
@@ -201,18 +249,25 @@ type FetchResponse<
201
249
  response: Response;
202
250
  };
203
251
 
204
- type RequestOptions<T> = ParamsOption<T> &
252
+ type RequestOptions<T> = OperationRequestOptions<T> & SharedRequestOptions;
253
+
254
+ /** The options of a call that depend on its operation `T`. */
255
+ type OperationRequestOptions<T> = ParamsOption<T> &
205
256
  RequestBodyOption<T> & {
206
- baseUrl?: string | undefined;
207
257
  querySerializer?: QuerySerializer<T> | QuerySerializerOptions | undefined;
208
258
  bodySerializer?: BodySerializer<T> | undefined;
209
- pathSerializer?: PathSerializer | undefined;
210
- parseAs?: ParseAs | undefined;
211
- fetch?: ClientOptions["fetch"];
212
- headers?: HeadersOptions | undefined;
213
- middleware?: Middleware[] | undefined;
214
259
  };
215
260
 
261
+ /** The options of a call that are the same for every operation. */
262
+ type SharedRequestOptions = {
263
+ baseUrl?: string | undefined;
264
+ pathSerializer?: PathSerializer | undefined;
265
+ parseAs?: ParseAs | undefined;
266
+ fetch?: ClientOptions["fetch"];
267
+ headers?: HeadersOptions | undefined;
268
+ middleware?: Middleware[] | undefined;
269
+ };
270
+
216
271
  type MergedOptions<T = unknown> = {
217
272
  baseUrl: string;
218
273
  parseAs: ParseAs;
package/dist/index.d.ts CHANGED
@@ -129,16 +129,58 @@ type BodyType<T = unknown> = {
129
129
  };
130
130
  type ParseAs = keyof BodyType;
131
131
  /**
132
- * The data of a success body `T` read with the `parseAs` of `Options`. Each value of a union gives
133
- * its body, and `undefined` is the default `"json"`, as at runtime: a wrapper that passes on
134
- * `parseAs: "text" | undefined` gets `string | T`, not `T` only. An optional `parseAs` (the
135
- * declared init type, as for a call without options) gives `T`, as in openapi-fetch. No `infer`:
136
- * TypeScript relates a type to a deferred conditional type through both of its branches only
137
- * when it infers nothing, and generic hooks return the data of a call as this type.
132
+ * The data of a success body `T` read with the `parseAs` of `Options`. Each value it may have gives
133
+ * its body, and `undefined` or a left-out `parseAs` is the default `"json"`, as at runtime: a
134
+ * wrapper that passes on `parseAs?: "text"` gets `string | T`, not `T` only. Options without
135
+ * `parseAs`, or whose `parseAs` may be any value (the declared init type, as for a call without
136
+ * options), give `T`, as in openapi-fetch. No `infer`: TypeScript relates a type to a deferred
137
+ * conditional type through both of its branches only when it infers nothing, and generic hooks
138
+ * return the data of a call as this type.
138
139
  */
139
- type ParseAsResponse<T, Options> = Options extends { parseAs: ParseAs | undefined }
140
- ? BodyType<T>[ParseAsValue<Options["parseAs"]>]
141
- : T;
140
+ type ParseAsResponse<T, Options> = [MissingParseAsValues<Options>] extends [never]
141
+ ? T
142
+ : BodyType<T>[ParseAsValue<ParseAsValues<Options>>];
143
+
144
+ /**
145
+ * The `parseAs` values that `Options` may have; `undefined` when it may be left out. One
146
+ * conditional type, not a union: `BodyType<T>` indexed by a union of generic keys is read as the
147
+ * intersection of their bodies when another type is checked against it. `any` options give `any`,
148
+ * so their data is `any`, as in openapi-fetch: a caller that passes an `any` init (as the generic
149
+ * hooks of openapi-typesafe-react-query do) returns it as the data type it declares. The branch
150
+ * is `any` itself, not `Options`: a type parameter there stops TypeScript relating the data of a
151
+ * generic call to a wrapper's (ADR-030).
152
+ */
153
+ type ParseAsValues<Options> = Options extends unknown
154
+ ? IsAny<Options> extends true
155
+ ? // biome-ignore lint/suspicious/noExplicitAny: any options give any data, as in openapi-fetch
156
+ any
157
+ :
158
+ | (Options extends RequiredParseAs ? never : undefined)
159
+ | { [Value in ParseAs]: Options extends WithoutParseAs<Value> ? never : Value }[ParseAs]
160
+ : never;
161
+
162
+ /**
163
+ * The `parseAs` values that `Options` cannot have, as `ParseAsValues` reads them. It does not
164
+ * distribute: a union of options cannot have a value only when none of its members can.
165
+ */
166
+ type MissingParseAsValues<Options> =
167
+ | ([Options] extends [RequiredParseAs] ? undefined : never)
168
+ | { [Value in ParseAs]: [Options] extends [WithoutParseAs<Value>] ? Value : never }[ParseAs];
169
+
170
+ /** Options that always have a `parseAs`. */
171
+ type RequiredParseAs = { parseAs: ParseAs };
172
+
173
+ /**
174
+ * Options that cannot have the `parseAs` value `Value`: `undefined` options, or options whose
175
+ * `parseAs` (if any) leaves it out. Each
176
+ * check takes `Options` as its checked type and a fixed type here, so TypeScript relates the
177
+ * data of a generic call to a wrapper's (ADR-030); one union of these types instead would let
178
+ * TypeScript split `parseAs?: ParseAs` into its values and match each one. The `object` keeps
179
+ * options without `parseAs` (`{ headers }`) from failing as a weak type.
180
+ */
181
+ type WithoutParseAs<Value extends ParseAs> =
182
+ | undefined
183
+ | (object & { parseAs?: Exclude<ParseAs, Value> | undefined });
142
184
 
143
185
  /** How a `parseAs` value reads the body, for each member of a union; `undefined` reads JSON. */
144
186
  type ParseAsValue<Value> = Value extends ParseAs ? Value : "json";
@@ -178,10 +220,16 @@ type RequestBodyOption<T> =
178
220
  * gave a conditional type no `FetchOptions` argument was assignable to (TS2345). The operations
179
221
  * of a union are not merged into one deep init type, which gave TS2589 on hundreds of paths
180
222
  * (upstream #1882).
223
+ * Only the options that depend on the operation distribute; the others stay outside the
224
+ * conditional type. While `request()` has not fixed its method, TypeScript reads the init's
225
+ * contextual type through the constraints of the method and the path, and stops after ten nested
226
+ * instantiations of one conditional type: with every option inside it, `parseAs: "text"` had no
227
+ * contextual type, widened to `string`, and the init fell back to its constraint (`data` the JSON
228
+ * body). `FetchOptions<never>` is still `never`.
181
229
  */
182
- type FetchOptions<T> = T extends unknown
183
- ? RequestOptions<T> & Omit<RequestInit, "body" | "headers">
184
- : never;
230
+ type FetchOptions<T> = (T extends unknown ? OperationRequestOptions<T> : never) &
231
+ SharedRequestOptions &
232
+ Omit<RequestInit, "body" | "headers">;
185
233
 
186
234
  // Readable<T> strips $Write markers (writeOnly properties excluded from response)
187
235
  type FetchResponse<
@@ -201,18 +249,25 @@ type FetchResponse<
201
249
  response: Response;
202
250
  };
203
251
 
204
- type RequestOptions<T> = ParamsOption<T> &
252
+ type RequestOptions<T> = OperationRequestOptions<T> & SharedRequestOptions;
253
+
254
+ /** The options of a call that depend on its operation `T`. */
255
+ type OperationRequestOptions<T> = ParamsOption<T> &
205
256
  RequestBodyOption<T> & {
206
- baseUrl?: string | undefined;
207
257
  querySerializer?: QuerySerializer<T> | QuerySerializerOptions | undefined;
208
258
  bodySerializer?: BodySerializer<T> | undefined;
209
- pathSerializer?: PathSerializer | undefined;
210
- parseAs?: ParseAs | undefined;
211
- fetch?: ClientOptions["fetch"];
212
- headers?: HeadersOptions | undefined;
213
- middleware?: Middleware[] | undefined;
214
259
  };
215
260
 
261
+ /** The options of a call that are the same for every operation. */
262
+ type SharedRequestOptions = {
263
+ baseUrl?: string | undefined;
264
+ pathSerializer?: PathSerializer | undefined;
265
+ parseAs?: ParseAs | undefined;
266
+ fetch?: ClientOptions["fetch"];
267
+ headers?: HeadersOptions | undefined;
268
+ middleware?: Middleware[] | undefined;
269
+ };
270
+
216
271
  type MergedOptions<T = unknown> = {
217
272
  baseUrl: string;
218
273
  parseAs: ParseAs;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "openapi-typesafe-fetch",
3
3
  "description": "Fast, type-safe fetch client for your OpenAPI schema. About 10.5 kB minified, 4 kB gzipped. Works with React, Vue, Svelte, or vanilla JS.",
4
- "version": "1.0.0-rc.10",
4
+ "version": "1.0.0-rc.11",
5
5
  "author": {
6
6
  "name": "Fedor Chishchin",
7
7
  "url": "https://github.com/fedorello"
@@ -67,7 +67,7 @@
67
67
  "express": "5.3.0",
68
68
  "feature-fetch": "0.1.2",
69
69
  "node-forge": "1.4.3",
70
- "openapi-typesafe": "^1.0.0-rc.10",
70
+ "openapi-typesafe": "^1.0.0-rc.11",
71
71
  "openapi-typescript-fetch": "2.2.1",
72
72
  "typescript": "^6.0.3",
73
73
  "undici": "7.30.2",
package/src/helpers.ts CHANGED
@@ -394,17 +394,39 @@ type TextErrorMediaTypes<Content> = {
394
394
  [Key in keyof Content]: IsJSONMediaPattern<Key> extends true ? never : Key;
395
395
  }[keyof Content];
396
396
 
397
- /** Whether the client parses every body of the media type `Key` as JSON, in any letter case. */
397
+ /**
398
+ * Whether the client parses every body of the media type `Key` as JSON, in any letter case: its
399
+ * subtype is `json` or ends in `+json`, as `JSON_MEDIA_TYPE_RE` reads it. A `json` in a parameter
400
+ * (`text/plain; profile=urn:a+json`) does not count; `JSONMediaPattern` alone matched it.
401
+ */
398
402
  type IsJSONMediaPattern<Key> = Key extends string
399
- ? Lowercase<Key> extends JSONMediaPattern
403
+ ? MediaSubtype<Lowercase<Key>> extends "json" | `${string}+json`
400
404
  ? true
401
405
  : false
402
406
  : false;
403
407
 
408
+ /**
409
+ * The subtype of a media type: the text after the first `/` and before the parameters (the first
410
+ * `;`), without the spaces and tabs at its end. Empty without a type before a `/`, as
411
+ * `JSON_MEDIA_TYPE_RE` wants one. A subtype may hold a `/`, as the regular expression allows.
412
+ */
413
+ type MediaSubtype<Text extends string> = Text extends `${infer Type}/${infer Rest}`
414
+ ? Type extends ""
415
+ ? ""
416
+ : TrimmedEnd<Rest extends `${infer Subtype};${string}` ? Subtype : Rest>
417
+ : "";
418
+
419
+ /** The text without the spaces and tabs at its end (OWS before parameters, RFC 9110). */
420
+ type TrimmedEnd<Text extends string> = Text extends `${infer Rest} ` | `${infer Rest}\t`
421
+ ? TrimmedEnd<Rest>
422
+ : Text;
423
+
404
424
  /**
405
425
  * The media types that the client parses as JSON, as a content map writes them: `json` or a
406
426
  * `+json` suffix (RFC 6838), with or without parameters, as the client's `JSON_MEDIA_TYPE_RE`.
407
- * Not `x-ndjson` or `json-seq`, whose bodies are lines of JSON.
427
+ * Not `x-ndjson` or `json-seq`, whose bodies are lines of JSON. A template cannot keep its
428
+ * `${string}` out of the parameters, so it only picks the schema of an index signature
429
+ * (`JSONMediaKey`); whether a media type is JSON is `IsJSONMediaPattern`'s answer.
408
430
  */
409
431
  type JSONMediaPattern =
410
432
  | `${string}/${"" | `${string}+`}json`
@@ -419,27 +441,52 @@ type JSONMediaRange = `${string}/*` | `${string}/*${";" | " " | "\t"}${string}`;
419
441
  /** The media types that may hold JSON, in lower case. */
420
442
  type JSONMediaType = JSONMediaPattern | JSONMediaRange;
421
443
 
422
- /** Whether the client parses a body of the media type `Key` as JSON, in any letter case. */
444
+ /**
445
+ * Whether a body of the media type `Key` may be one the client parses as JSON, in any letter
446
+ * case: a JSON media type (`IsJSONMediaPattern`) or a media range (subtype `*`), with or without
447
+ * parameters, which do not count.
448
+ */
423
449
  type IsJSONMediaType<Key> = Key extends string
424
- ? Lowercase<Key> extends JSONMediaType
450
+ ? IsJSONMediaPattern<Key> extends true
425
451
  ? true
426
- : false
452
+ : MediaSubtype<Lowercase<Key>> extends "*"
453
+ ? true
454
+ : false
427
455
  : false;
428
456
 
429
457
  /** Return first JSON-like 2XX response from a path + HTTP method */
430
458
  // biome-ignore lint/suspicious/noExplicitAny: unknown rejects the generated interfaces
431
459
  export type SuccessResponseJSON<PathMethod extends Record<string | number, any>> = SuccessResponse<
432
460
  ResponseObjectMap<PathMethod>,
433
- JSONMediaType
461
+ JSONMediaKeys<ResponseObjectMap<PathMethod>>
434
462
  >;
435
463
 
436
464
  /** Return JSON-like 1XX, 3XX, 4XX, 5XX and `default` responses from a path + HTTP method */
437
465
  // biome-ignore lint/suspicious/noExplicitAny: unknown rejects the generated interfaces
438
466
  export type ErrorResponseJSON<PathMethod extends Record<string | number, any>> = ErrorResponse<
439
467
  ResponseObjectMap<PathMethod>,
440
- JSONMediaType
468
+ JSONMediaKeys<ResponseObjectMap<PathMethod>>
441
469
  >;
442
470
 
471
+ /**
472
+ * The media types of the responses `T` that may hold JSON (`IsJSONMediaType`), for the JSON
473
+ * helpers to pick by: their keys, as `JSONMediaType` cannot leave out a `json` in parameters. A
474
+ * content map with an index signature has no such key, so it gives `JSONMediaType`, which picks
475
+ * the schema of its signature.
476
+ */
477
+ type JSONMediaKeys<T> = {
478
+ [Status in keyof T]: T[Status] extends { content: infer Content }
479
+ ? { [Key in keyof Content]: JSONMediaKey<Key> }[keyof Content]
480
+ : never;
481
+ }[keyof T];
482
+
483
+ /** `Key` when it may hold JSON; `JSONMediaType` for the wide key of an index signature. */
484
+ type JSONMediaKey<Key> = string extends Key
485
+ ? JSONMediaType
486
+ : IsJSONMediaType<Key> extends true
487
+ ? Key
488
+ : never;
489
+
443
490
  /** Return JSON-like request body from a path + HTTP method */
444
491
  export type RequestBodyJSON<PathMethod> = JSONLike<
445
492
  FilterKeys<OperationRequestBody<PathMethod>, "content">
package/src/index.d.ts CHANGED
@@ -139,16 +139,58 @@ type BodyType<T = unknown> = {
139
139
  };
140
140
  export type ParseAs = keyof BodyType;
141
141
  /**
142
- * The data of a success body `T` read with the `parseAs` of `Options`. Each value of a union gives
143
- * its body, and `undefined` is the default `"json"`, as at runtime: a wrapper that passes on
144
- * `parseAs: "text" | undefined` gets `string | T`, not `T` only. An optional `parseAs` (the
145
- * declared init type, as for a call without options) gives `T`, as in openapi-fetch. No `infer`:
146
- * TypeScript relates a type to a deferred conditional type through both of its branches only
147
- * when it infers nothing, and generic hooks return the data of a call as this type.
142
+ * The data of a success body `T` read with the `parseAs` of `Options`. Each value it may have gives
143
+ * its body, and `undefined` or a left-out `parseAs` is the default `"json"`, as at runtime: a
144
+ * wrapper that passes on `parseAs?: "text"` gets `string | T`, not `T` only. Options without
145
+ * `parseAs`, or whose `parseAs` may be any value (the declared init type, as for a call without
146
+ * options), give `T`, as in openapi-fetch. No `infer`: TypeScript relates a type to a deferred
147
+ * conditional type through both of its branches only when it infers nothing, and generic hooks
148
+ * return the data of a call as this type.
148
149
  */
149
- export type ParseAsResponse<T, Options> = Options extends { parseAs: ParseAs | undefined }
150
- ? BodyType<T>[ParseAsValue<Options["parseAs"]>]
151
- : T;
150
+ export type ParseAsResponse<T, Options> = [MissingParseAsValues<Options>] extends [never]
151
+ ? T
152
+ : BodyType<T>[ParseAsValue<ParseAsValues<Options>>];
153
+
154
+ /**
155
+ * The `parseAs` values that `Options` may have; `undefined` when it may be left out. One
156
+ * conditional type, not a union: `BodyType<T>` indexed by a union of generic keys is read as the
157
+ * intersection of their bodies when another type is checked against it. `any` options give `any`,
158
+ * so their data is `any`, as in openapi-fetch: a caller that passes an `any` init (as the generic
159
+ * hooks of openapi-typesafe-react-query do) returns it as the data type it declares. The branch
160
+ * is `any` itself, not `Options`: a type parameter there stops TypeScript relating the data of a
161
+ * generic call to a wrapper's (ADR-030).
162
+ */
163
+ type ParseAsValues<Options> = Options extends unknown
164
+ ? IsAny<Options> extends true
165
+ ? // biome-ignore lint/suspicious/noExplicitAny: any options give any data, as in openapi-fetch
166
+ any
167
+ :
168
+ | (Options extends RequiredParseAs ? never : undefined)
169
+ | { [Value in ParseAs]: Options extends WithoutParseAs<Value> ? never : Value }[ParseAs]
170
+ : never;
171
+
172
+ /**
173
+ * The `parseAs` values that `Options` cannot have, as `ParseAsValues` reads them. It does not
174
+ * distribute: a union of options cannot have a value only when none of its members can.
175
+ */
176
+ type MissingParseAsValues<Options> =
177
+ | ([Options] extends [RequiredParseAs] ? undefined : never)
178
+ | { [Value in ParseAs]: [Options] extends [WithoutParseAs<Value>] ? Value : never }[ParseAs];
179
+
180
+ /** Options that always have a `parseAs`. */
181
+ type RequiredParseAs = { parseAs: ParseAs };
182
+
183
+ /**
184
+ * Options that cannot have the `parseAs` value `Value`: `undefined` options, or options whose
185
+ * `parseAs` (if any) leaves it out. Each
186
+ * check takes `Options` as its checked type and a fixed type here, so TypeScript relates the
187
+ * data of a generic call to a wrapper's (ADR-030); one union of these types instead would let
188
+ * TypeScript split `parseAs?: ParseAs` into its values and match each one. The `object` keeps
189
+ * options without `parseAs` (`{ headers }`) from failing as a weak type.
190
+ */
191
+ type WithoutParseAs<Value extends ParseAs> =
192
+ | undefined
193
+ | (object & { parseAs?: Exclude<ParseAs, Value> | undefined });
152
194
 
153
195
  /** How a `parseAs` value reads the body, for each member of a union; `undefined` reads JSON. */
154
196
  type ParseAsValue<Value> = Value extends ParseAs ? Value : "json";
@@ -188,10 +230,16 @@ export type RequestBodyOption<T> =
188
230
  * gave a conditional type no `FetchOptions` argument was assignable to (TS2345). The operations
189
231
  * of a union are not merged into one deep init type, which gave TS2589 on hundreds of paths
190
232
  * (upstream #1882).
233
+ * Only the options that depend on the operation distribute; the others stay outside the
234
+ * conditional type. While `request()` has not fixed its method, TypeScript reads the init's
235
+ * contextual type through the constraints of the method and the path, and stops after ten nested
236
+ * instantiations of one conditional type: with every option inside it, `parseAs: "text"` had no
237
+ * contextual type, widened to `string`, and the init fell back to its constraint (`data` the JSON
238
+ * body). `FetchOptions<never>` is still `never`.
191
239
  */
192
- export type FetchOptions<T> = T extends unknown
193
- ? RequestOptions<T> & Omit<RequestInit, "body" | "headers">
194
- : never;
240
+ export type FetchOptions<T> = (T extends unknown ? OperationRequestOptions<T> : never) &
241
+ SharedRequestOptions &
242
+ Omit<RequestInit, "body" | "headers">;
195
243
 
196
244
  // Readable<T> strips $Write markers (writeOnly properties excluded from response)
197
245
  export type FetchResponse<
@@ -211,18 +259,25 @@ export type FetchResponse<
211
259
  response: Response;
212
260
  };
213
261
 
214
- export type RequestOptions<T> = ParamsOption<T> &
262
+ export type RequestOptions<T> = OperationRequestOptions<T> & SharedRequestOptions;
263
+
264
+ /** The options of a call that depend on its operation `T`. */
265
+ type OperationRequestOptions<T> = ParamsOption<T> &
215
266
  RequestBodyOption<T> & {
216
- baseUrl?: string | undefined;
217
267
  querySerializer?: QuerySerializer<T> | QuerySerializerOptions | undefined;
218
268
  bodySerializer?: BodySerializer<T> | undefined;
219
- pathSerializer?: PathSerializer | undefined;
220
- parseAs?: ParseAs | undefined;
221
- fetch?: ClientOptions["fetch"];
222
- headers?: HeadersOptions | undefined;
223
- middleware?: Middleware[] | undefined;
224
269
  };
225
270
 
271
+ /** The options of a call that are the same for every operation. */
272
+ type SharedRequestOptions = {
273
+ baseUrl?: string | undefined;
274
+ pathSerializer?: PathSerializer | undefined;
275
+ parseAs?: ParseAs | undefined;
276
+ fetch?: ClientOptions["fetch"];
277
+ headers?: HeadersOptions | undefined;
278
+ middleware?: Middleware[] | undefined;
279
+ };
280
+
226
281
  export type MergedOptions<T = unknown> = {
227
282
  baseUrl: string;
228
283
  parseAs: ParseAs;