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

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,42 @@
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.12
6
+
7
+ ### Patch Changes
8
+
9
+ - [#136](https://github.com/openapi-typesafe/openapi-typesafe/pull/136) [`68ce29a`](https://github.com/openapi-typesafe/openapi-typesafe/commit/68ce29ad4b8f8490d38990af62bbc830b54df015) Thanks [@fedorello](https://github.com/fedorello)! - A wrapper generic in the path that sets `parseAs`, such as
10
+ `client.GET(path, { ...init, parseAs: "text" })` or an `init` typed
11
+ `FetchOptions<…> & { parseAs: "text" }`, types `data` as the body of that `parseAs` again
12
+ (`string` for `"text"`), as openapi-fetch does. Before, it was the text or any JSON body of the
13
+ paths, so assigning it to `string | undefined` failed. Each member of a union of options now
14
+ types its own data: `{ parseAs: ParseAs } | undefined` gives every body, as in openapi-fetch.
15
+
16
+ - [#136](https://github.com/openapi-typesafe/openapi-typesafe/pull/136) [`68ce29a`](https://github.com/openapi-typesafe/openapi-typesafe/commit/68ce29ad4b8f8490d38990af62bbc830b54df015) Thanks [@fedorello](https://github.com/fedorello)! - `SuccessResponseJSON` and `ErrorResponseJSON` of a union of operations are the union of each
17
+ operation's JSON bodies again. Before, only the status codes the operations share counted, so
18
+ `ErrorResponseJSON<OpA | OpB>` could gain a `string` that neither operation has alone.
19
+
20
+ ## 1.0.0-rc.11
21
+
22
+ ### Patch Changes
23
+
24
+ - [#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,
25
+ as at runtime: an error declared as `text/plain; profile=urn:a+json` or
26
+ `message/http; msgtype=application/json` is typed as the text (`string`) the client returns, and
27
+ `JSONLike`, `RequestBodyJSON`, `SuccessResponseJSON` and `ErrorResponseJSON` leave such media
28
+ types out. Only the media type before the `;` counts, for media ranges (`text/*`) too.
29
+
30
+ - [#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" }`
31
+ passed to `client.GET()`, types `data` as the body of each value or the JSON body (`string | T`),
32
+ as the client returns it. Before, it was typed as the JSON body only. Options whose `parseAs` may
33
+ be any value, or that have none, still type `data` as the JSON body. With and without
34
+ `exactOptionalPropertyTypes`.
35
+
36
+ - [#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
37
+ other method calls do: `{ parseAs: "text" }` gives a `string`, `"stream"` the body stream. Before,
38
+ `data` was always the JSON body. A literal in `params` or `body` next to `parseAs`, such as an
39
+ enum value, no longer loses the `parseAs` type either.
40
+
5
41
  ## 1.0.0-rc.10
6
42
 
7
43
  ### 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,39 @@ 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;
292
- /** Return first JSON-like 2XX response from a path + HTTP method */
293
- type SuccessResponseJSON<PathMethod extends Record<string | number, any>> = SuccessResponse<ResponseObjectMap<PathMethod>, JSONMediaType>;
294
- /** 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>;
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;
310
+ /**
311
+ * Return first JSON-like 2XX response from a path + HTTP method; for a union of operations, the
312
+ * union of each one's.
313
+ */
314
+ type SuccessResponseJSON<PathMethod extends Record<string | number, any>> = PathMethod extends unknown ? SuccessResponse<ResponseObjectMap<PathMethod>, JSONMediaKeys<ResponseObjectMap<PathMethod>>> : never;
315
+ /**
316
+ * Return JSON-like 1XX, 3XX, 4XX, 5XX and `default` responses from a path + HTTP method; for a
317
+ * union of operations, the union of each one's.
318
+ */
319
+ type ErrorResponseJSON<PathMethod extends Record<string | number, any>> = PathMethod extends unknown ? ErrorResponse<ResponseObjectMap<PathMethod>, JSONMediaKeys<ResponseObjectMap<PathMethod>>> : never;
320
+ /**
321
+ * The media types of the responses `T` (of one operation) that may hold JSON (`IsJSONMediaType`),
322
+ * for the JSON helpers to pick by: their keys, as `JSONMediaType` cannot leave out a `json` in
323
+ * parameters. A content map with an index signature has no such key, so it gives
324
+ * `JSONMediaType`, which picks the schema of its signature. The helpers pick per operation: the
325
+ * keys of a union of response maps are only the status codes they share, so the media types of
326
+ * the other statuses were lost, and the media types of one operation could pick in another.
327
+ */
328
+ type JSONMediaKeys<T> = {
329
+ [Status in keyof T]: T[Status] extends {
330
+ content: infer Content;
331
+ } ? {
332
+ [Key in keyof Content]: JSONMediaKey<Key>;
333
+ }[keyof Content] : never;
334
+ }[keyof T];
335
+ /** `Key` when it may hold JSON; `JSONMediaType` for the wide key of an index signature. */
336
+ type JSONMediaKey<Key> = string extends Key ? JSONMediaType : IsJSONMediaType<Key> extends true ? Key : never;
296
337
  /** Return JSON-like request body from a path + HTTP method */
297
338
  type RequestBodyJSON<PathMethod> = JSONLike<FilterKeys<OperationRequestBody<PathMethod>, "content">>;
298
339
  /** 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,39 @@ 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;
292
- /** Return first JSON-like 2XX response from a path + HTTP method */
293
- type SuccessResponseJSON<PathMethod extends Record<string | number, any>> = SuccessResponse<ResponseObjectMap<PathMethod>, JSONMediaType>;
294
- /** 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>;
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;
310
+ /**
311
+ * Return first JSON-like 2XX response from a path + HTTP method; for a union of operations, the
312
+ * union of each one's.
313
+ */
314
+ type SuccessResponseJSON<PathMethod extends Record<string | number, any>> = PathMethod extends unknown ? SuccessResponse<ResponseObjectMap<PathMethod>, JSONMediaKeys<ResponseObjectMap<PathMethod>>> : never;
315
+ /**
316
+ * Return JSON-like 1XX, 3XX, 4XX, 5XX and `default` responses from a path + HTTP method; for a
317
+ * union of operations, the union of each one's.
318
+ */
319
+ type ErrorResponseJSON<PathMethod extends Record<string | number, any>> = PathMethod extends unknown ? ErrorResponse<ResponseObjectMap<PathMethod>, JSONMediaKeys<ResponseObjectMap<PathMethod>>> : never;
320
+ /**
321
+ * The media types of the responses `T` (of one operation) that may hold JSON (`IsJSONMediaType`),
322
+ * for the JSON helpers to pick by: their keys, as `JSONMediaType` cannot leave out a `json` in
323
+ * parameters. A content map with an index signature has no such key, so it gives
324
+ * `JSONMediaType`, which picks the schema of its signature. The helpers pick per operation: the
325
+ * keys of a union of response maps are only the status codes they share, so the media types of
326
+ * the other statuses were lost, and the media types of one operation could pick in another.
327
+ */
328
+ type JSONMediaKeys<T> = {
329
+ [Status in keyof T]: T[Status] extends {
330
+ content: infer Content;
331
+ } ? {
332
+ [Key in keyof Content]: JSONMediaKey<Key>;
333
+ }[keyof Content] : never;
334
+ }[keyof T];
335
+ /** `Key` when it may hold JSON; `JSONMediaType` for the wide key of an index signature. */
336
+ type JSONMediaKey<Key> = string extends Key ? JSONMediaType : IsJSONMediaType<Key> extends true ? Key : never;
296
337
  /** Return JSON-like request body from a path + HTTP method */
297
338
  type RequestBodyJSON<PathMethod> = JSONLike<FilterKeys<OperationRequestBody<PathMethod>, "content">>;
298
339
  /** 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,39 @@ 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;
292
- /** Return first JSON-like 2XX response from a path + HTTP method */
293
- type SuccessResponseJSON<PathMethod extends Record<string | number, any>> = SuccessResponse<ResponseObjectMap<PathMethod>, JSONMediaType>;
294
- /** 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>;
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;
310
+ /**
311
+ * Return first JSON-like 2XX response from a path + HTTP method; for a union of operations, the
312
+ * union of each one's.
313
+ */
314
+ type SuccessResponseJSON<PathMethod extends Record<string | number, any>> = PathMethod extends unknown ? SuccessResponse<ResponseObjectMap<PathMethod>, JSONMediaKeys<ResponseObjectMap<PathMethod>>> : never;
315
+ /**
316
+ * Return JSON-like 1XX, 3XX, 4XX, 5XX and `default` responses from a path + HTTP method; for a
317
+ * union of operations, the union of each one's.
318
+ */
319
+ type ErrorResponseJSON<PathMethod extends Record<string | number, any>> = PathMethod extends unknown ? ErrorResponse<ResponseObjectMap<PathMethod>, JSONMediaKeys<ResponseObjectMap<PathMethod>>> : never;
320
+ /**
321
+ * The media types of the responses `T` (of one operation) that may hold JSON (`IsJSONMediaType`),
322
+ * for the JSON helpers to pick by: their keys, as `JSONMediaType` cannot leave out a `json` in
323
+ * parameters. A content map with an index signature has no such key, so it gives
324
+ * `JSONMediaType`, which picks the schema of its signature. The helpers pick per operation: the
325
+ * keys of a union of response maps are only the status codes they share, so the media types of
326
+ * the other statuses were lost, and the media types of one operation could pick in another.
327
+ */
328
+ type JSONMediaKeys<T> = {
329
+ [Status in keyof T]: T[Status] extends {
330
+ content: infer Content;
331
+ } ? {
332
+ [Key in keyof Content]: JSONMediaKey<Key>;
333
+ }[keyof Content] : never;
334
+ }[keyof T];
335
+ /** `Key` when it may hold JSON; `JSONMediaType` for the wide key of an index signature. */
336
+ type JSONMediaKey<Key> = string extends Key ? JSONMediaType : IsJSONMediaType<Key> extends true ? Key : never;
296
337
  /** Return JSON-like request body from a path + HTTP method */
297
338
  type RequestBodyJSON<PathMethod> = JSONLike<FilterKeys<OperationRequestBody<PathMethod>, "content">>;
298
339
  /** Find first match of multiple keys */
package/dist/index.d.cts CHANGED
@@ -129,16 +129,71 @@ 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; each member of a union of options gives its own data.
137
+ * No `infer`: TypeScript relates a type to a deferred conditional type through both of its
138
+ * branches only when it infers nothing, and generic hooks return the data of a call as this type.
139
+ * It distributes over `Options`, as openapi-fetch's does, so the options of a generic call (a
140
+ * wrapper's `FetchOptions` of a generic path with `parseAs: "text"`) give the data of their
141
+ * constraint: TypeScript reads a deferred distributive type through its check type's constraint.
138
142
  */
139
- type ParseAsResponse<T, Options> = Options extends { parseAs: ParseAs | undefined }
140
- ? BodyType<T>[ParseAsValue<Options["parseAs"]>]
141
- : T;
143
+ type ParseAsResponse<T, Options> = Options extends unknown
144
+ ? BodyType<T>[ParseAsKey<Options>]
145
+ : never;
146
+
147
+ /**
148
+ * The key of `BodyType` that the options `Options` read the body with. A conditional type of its
149
+ * own, inside the index: one in the branch of `ParseAsResponse` that picked `T` would be read
150
+ * through both of its branches while TypeScript reads the constraint above, as `never` fits any
151
+ * check type, so a generic `parseAs: "text"` gave the JSON body too.
152
+ */
153
+ type ParseAsKey<Options> = [MissingParseAsValues<Options>] extends [never]
154
+ ? "json"
155
+ : ParseAsValue<ParseAsValues<Options>>;
156
+
157
+ /**
158
+ * The `parseAs` values that `Options` may have; `undefined` when it may be left out. One
159
+ * conditional type, not a union: `BodyType<T>` indexed by a union of generic keys is read as the
160
+ * intersection of their bodies when another type is checked against it. `any` options give `any`,
161
+ * so their data is `any`, as in openapi-fetch: a caller that passes an `any` init (as the generic
162
+ * hooks of openapi-typesafe-react-query do) returns it as the data type it declares. The branch
163
+ * is `any` itself, not `Options`: a type parameter there stops TypeScript relating the data of a
164
+ * generic call to a wrapper's (ADR-030).
165
+ */
166
+ type ParseAsValues<Options> = Options extends unknown
167
+ ? IsAny<Options> extends true
168
+ ? // biome-ignore lint/suspicious/noExplicitAny: any options give any data, as in openapi-fetch
169
+ any
170
+ :
171
+ | (Options extends RequiredParseAs ? never : undefined)
172
+ | { [Value in ParseAs]: Options extends WithoutParseAs<Value> ? never : Value }[ParseAs]
173
+ : never;
174
+
175
+ /**
176
+ * The `parseAs` values that the options `Options` (one member of a union) cannot have, as
177
+ * `ParseAsValues` reads them.
178
+ */
179
+ type MissingParseAsValues<Options> =
180
+ | ([Options] extends [RequiredParseAs] ? undefined : never)
181
+ | { [Value in ParseAs]: [Options] extends [WithoutParseAs<Value>] ? Value : never }[ParseAs];
182
+
183
+ /** Options that always have a `parseAs`. */
184
+ type RequiredParseAs = { parseAs: ParseAs };
185
+
186
+ /**
187
+ * Options that cannot have the `parseAs` value `Value`: `undefined` options, or options whose
188
+ * `parseAs` (if any) leaves it out. Each
189
+ * check takes `Options` as its checked type and a fixed type here, so TypeScript relates the
190
+ * data of a generic call to a wrapper's (ADR-030); one union of these types instead would let
191
+ * TypeScript split `parseAs?: ParseAs` into its values and match each one. The `object` keeps
192
+ * options without `parseAs` (`{ headers }`) from failing as a weak type.
193
+ */
194
+ type WithoutParseAs<Value extends ParseAs> =
195
+ | undefined
196
+ | (object & { parseAs?: Exclude<ParseAs, Value> | undefined });
142
197
 
143
198
  /** How a `parseAs` value reads the body, for each member of a union; `undefined` reads JSON. */
144
199
  type ParseAsValue<Value> = Value extends ParseAs ? Value : "json";
@@ -178,10 +233,16 @@ type RequestBodyOption<T> =
178
233
  * gave a conditional type no `FetchOptions` argument was assignable to (TS2345). The operations
179
234
  * of a union are not merged into one deep init type, which gave TS2589 on hundreds of paths
180
235
  * (upstream #1882).
236
+ * Only the options that depend on the operation distribute; the others stay outside the
237
+ * conditional type. While `request()` has not fixed its method, TypeScript reads the init's
238
+ * contextual type through the constraints of the method and the path, and stops after ten nested
239
+ * instantiations of one conditional type: with every option inside it, `parseAs: "text"` had no
240
+ * contextual type, widened to `string`, and the init fell back to its constraint (`data` the JSON
241
+ * body). `FetchOptions<never>` is still `never`.
181
242
  */
182
- type FetchOptions<T> = T extends unknown
183
- ? RequestOptions<T> & Omit<RequestInit, "body" | "headers">
184
- : never;
243
+ type FetchOptions<T> = (T extends unknown ? OperationRequestOptions<T> : never) &
244
+ SharedRequestOptions &
245
+ Omit<RequestInit, "body" | "headers">;
185
246
 
186
247
  // Readable<T> strips $Write markers (writeOnly properties excluded from response)
187
248
  type FetchResponse<
@@ -201,18 +262,25 @@ type FetchResponse<
201
262
  response: Response;
202
263
  };
203
264
 
204
- type RequestOptions<T> = ParamsOption<T> &
265
+ type RequestOptions<T> = OperationRequestOptions<T> & SharedRequestOptions;
266
+
267
+ /** The options of a call that depend on its operation `T`. */
268
+ type OperationRequestOptions<T> = ParamsOption<T> &
205
269
  RequestBodyOption<T> & {
206
- baseUrl?: string | undefined;
207
270
  querySerializer?: QuerySerializer<T> | QuerySerializerOptions | undefined;
208
271
  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
272
  };
215
273
 
274
+ /** The options of a call that are the same for every operation. */
275
+ type SharedRequestOptions = {
276
+ baseUrl?: string | undefined;
277
+ pathSerializer?: PathSerializer | undefined;
278
+ parseAs?: ParseAs | undefined;
279
+ fetch?: ClientOptions["fetch"];
280
+ headers?: HeadersOptions | undefined;
281
+ middleware?: Middleware[] | undefined;
282
+ };
283
+
216
284
  type MergedOptions<T = unknown> = {
217
285
  baseUrl: string;
218
286
  parseAs: ParseAs;
package/dist/index.d.mts CHANGED
@@ -129,16 +129,71 @@ 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; each member of a union of options gives its own data.
137
+ * No `infer`: TypeScript relates a type to a deferred conditional type through both of its
138
+ * branches only when it infers nothing, and generic hooks return the data of a call as this type.
139
+ * It distributes over `Options`, as openapi-fetch's does, so the options of a generic call (a
140
+ * wrapper's `FetchOptions` of a generic path with `parseAs: "text"`) give the data of their
141
+ * constraint: TypeScript reads a deferred distributive type through its check type's constraint.
138
142
  */
139
- type ParseAsResponse<T, Options> = Options extends { parseAs: ParseAs | undefined }
140
- ? BodyType<T>[ParseAsValue<Options["parseAs"]>]
141
- : T;
143
+ type ParseAsResponse<T, Options> = Options extends unknown
144
+ ? BodyType<T>[ParseAsKey<Options>]
145
+ : never;
146
+
147
+ /**
148
+ * The key of `BodyType` that the options `Options` read the body with. A conditional type of its
149
+ * own, inside the index: one in the branch of `ParseAsResponse` that picked `T` would be read
150
+ * through both of its branches while TypeScript reads the constraint above, as `never` fits any
151
+ * check type, so a generic `parseAs: "text"` gave the JSON body too.
152
+ */
153
+ type ParseAsKey<Options> = [MissingParseAsValues<Options>] extends [never]
154
+ ? "json"
155
+ : ParseAsValue<ParseAsValues<Options>>;
156
+
157
+ /**
158
+ * The `parseAs` values that `Options` may have; `undefined` when it may be left out. One
159
+ * conditional type, not a union: `BodyType<T>` indexed by a union of generic keys is read as the
160
+ * intersection of their bodies when another type is checked against it. `any` options give `any`,
161
+ * so their data is `any`, as in openapi-fetch: a caller that passes an `any` init (as the generic
162
+ * hooks of openapi-typesafe-react-query do) returns it as the data type it declares. The branch
163
+ * is `any` itself, not `Options`: a type parameter there stops TypeScript relating the data of a
164
+ * generic call to a wrapper's (ADR-030).
165
+ */
166
+ type ParseAsValues<Options> = Options extends unknown
167
+ ? IsAny<Options> extends true
168
+ ? // biome-ignore lint/suspicious/noExplicitAny: any options give any data, as in openapi-fetch
169
+ any
170
+ :
171
+ | (Options extends RequiredParseAs ? never : undefined)
172
+ | { [Value in ParseAs]: Options extends WithoutParseAs<Value> ? never : Value }[ParseAs]
173
+ : never;
174
+
175
+ /**
176
+ * The `parseAs` values that the options `Options` (one member of a union) cannot have, as
177
+ * `ParseAsValues` reads them.
178
+ */
179
+ type MissingParseAsValues<Options> =
180
+ | ([Options] extends [RequiredParseAs] ? undefined : never)
181
+ | { [Value in ParseAs]: [Options] extends [WithoutParseAs<Value>] ? Value : never }[ParseAs];
182
+
183
+ /** Options that always have a `parseAs`. */
184
+ type RequiredParseAs = { parseAs: ParseAs };
185
+
186
+ /**
187
+ * Options that cannot have the `parseAs` value `Value`: `undefined` options, or options whose
188
+ * `parseAs` (if any) leaves it out. Each
189
+ * check takes `Options` as its checked type and a fixed type here, so TypeScript relates the
190
+ * data of a generic call to a wrapper's (ADR-030); one union of these types instead would let
191
+ * TypeScript split `parseAs?: ParseAs` into its values and match each one. The `object` keeps
192
+ * options without `parseAs` (`{ headers }`) from failing as a weak type.
193
+ */
194
+ type WithoutParseAs<Value extends ParseAs> =
195
+ | undefined
196
+ | (object & { parseAs?: Exclude<ParseAs, Value> | undefined });
142
197
 
143
198
  /** How a `parseAs` value reads the body, for each member of a union; `undefined` reads JSON. */
144
199
  type ParseAsValue<Value> = Value extends ParseAs ? Value : "json";
@@ -178,10 +233,16 @@ type RequestBodyOption<T> =
178
233
  * gave a conditional type no `FetchOptions` argument was assignable to (TS2345). The operations
179
234
  * of a union are not merged into one deep init type, which gave TS2589 on hundreds of paths
180
235
  * (upstream #1882).
236
+ * Only the options that depend on the operation distribute; the others stay outside the
237
+ * conditional type. While `request()` has not fixed its method, TypeScript reads the init's
238
+ * contextual type through the constraints of the method and the path, and stops after ten nested
239
+ * instantiations of one conditional type: with every option inside it, `parseAs: "text"` had no
240
+ * contextual type, widened to `string`, and the init fell back to its constraint (`data` the JSON
241
+ * body). `FetchOptions<never>` is still `never`.
181
242
  */
182
- type FetchOptions<T> = T extends unknown
183
- ? RequestOptions<T> & Omit<RequestInit, "body" | "headers">
184
- : never;
243
+ type FetchOptions<T> = (T extends unknown ? OperationRequestOptions<T> : never) &
244
+ SharedRequestOptions &
245
+ Omit<RequestInit, "body" | "headers">;
185
246
 
186
247
  // Readable<T> strips $Write markers (writeOnly properties excluded from response)
187
248
  type FetchResponse<
@@ -201,18 +262,25 @@ type FetchResponse<
201
262
  response: Response;
202
263
  };
203
264
 
204
- type RequestOptions<T> = ParamsOption<T> &
265
+ type RequestOptions<T> = OperationRequestOptions<T> & SharedRequestOptions;
266
+
267
+ /** The options of a call that depend on its operation `T`. */
268
+ type OperationRequestOptions<T> = ParamsOption<T> &
205
269
  RequestBodyOption<T> & {
206
- baseUrl?: string | undefined;
207
270
  querySerializer?: QuerySerializer<T> | QuerySerializerOptions | undefined;
208
271
  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
272
  };
215
273
 
274
+ /** The options of a call that are the same for every operation. */
275
+ type SharedRequestOptions = {
276
+ baseUrl?: string | undefined;
277
+ pathSerializer?: PathSerializer | undefined;
278
+ parseAs?: ParseAs | undefined;
279
+ fetch?: ClientOptions["fetch"];
280
+ headers?: HeadersOptions | undefined;
281
+ middleware?: Middleware[] | undefined;
282
+ };
283
+
216
284
  type MergedOptions<T = unknown> = {
217
285
  baseUrl: string;
218
286
  parseAs: ParseAs;
package/dist/index.d.ts CHANGED
@@ -129,16 +129,71 @@ 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; each member of a union of options gives its own data.
137
+ * No `infer`: TypeScript relates a type to a deferred conditional type through both of its
138
+ * branches only when it infers nothing, and generic hooks return the data of a call as this type.
139
+ * It distributes over `Options`, as openapi-fetch's does, so the options of a generic call (a
140
+ * wrapper's `FetchOptions` of a generic path with `parseAs: "text"`) give the data of their
141
+ * constraint: TypeScript reads a deferred distributive type through its check type's constraint.
138
142
  */
139
- type ParseAsResponse<T, Options> = Options extends { parseAs: ParseAs | undefined }
140
- ? BodyType<T>[ParseAsValue<Options["parseAs"]>]
141
- : T;
143
+ type ParseAsResponse<T, Options> = Options extends unknown
144
+ ? BodyType<T>[ParseAsKey<Options>]
145
+ : never;
146
+
147
+ /**
148
+ * The key of `BodyType` that the options `Options` read the body with. A conditional type of its
149
+ * own, inside the index: one in the branch of `ParseAsResponse` that picked `T` would be read
150
+ * through both of its branches while TypeScript reads the constraint above, as `never` fits any
151
+ * check type, so a generic `parseAs: "text"` gave the JSON body too.
152
+ */
153
+ type ParseAsKey<Options> = [MissingParseAsValues<Options>] extends [never]
154
+ ? "json"
155
+ : ParseAsValue<ParseAsValues<Options>>;
156
+
157
+ /**
158
+ * The `parseAs` values that `Options` may have; `undefined` when it may be left out. One
159
+ * conditional type, not a union: `BodyType<T>` indexed by a union of generic keys is read as the
160
+ * intersection of their bodies when another type is checked against it. `any` options give `any`,
161
+ * so their data is `any`, as in openapi-fetch: a caller that passes an `any` init (as the generic
162
+ * hooks of openapi-typesafe-react-query do) returns it as the data type it declares. The branch
163
+ * is `any` itself, not `Options`: a type parameter there stops TypeScript relating the data of a
164
+ * generic call to a wrapper's (ADR-030).
165
+ */
166
+ type ParseAsValues<Options> = Options extends unknown
167
+ ? IsAny<Options> extends true
168
+ ? // biome-ignore lint/suspicious/noExplicitAny: any options give any data, as in openapi-fetch
169
+ any
170
+ :
171
+ | (Options extends RequiredParseAs ? never : undefined)
172
+ | { [Value in ParseAs]: Options extends WithoutParseAs<Value> ? never : Value }[ParseAs]
173
+ : never;
174
+
175
+ /**
176
+ * The `parseAs` values that the options `Options` (one member of a union) cannot have, as
177
+ * `ParseAsValues` reads them.
178
+ */
179
+ type MissingParseAsValues<Options> =
180
+ | ([Options] extends [RequiredParseAs] ? undefined : never)
181
+ | { [Value in ParseAs]: [Options] extends [WithoutParseAs<Value>] ? Value : never }[ParseAs];
182
+
183
+ /** Options that always have a `parseAs`. */
184
+ type RequiredParseAs = { parseAs: ParseAs };
185
+
186
+ /**
187
+ * Options that cannot have the `parseAs` value `Value`: `undefined` options, or options whose
188
+ * `parseAs` (if any) leaves it out. Each
189
+ * check takes `Options` as its checked type and a fixed type here, so TypeScript relates the
190
+ * data of a generic call to a wrapper's (ADR-030); one union of these types instead would let
191
+ * TypeScript split `parseAs?: ParseAs` into its values and match each one. The `object` keeps
192
+ * options without `parseAs` (`{ headers }`) from failing as a weak type.
193
+ */
194
+ type WithoutParseAs<Value extends ParseAs> =
195
+ | undefined
196
+ | (object & { parseAs?: Exclude<ParseAs, Value> | undefined });
142
197
 
143
198
  /** How a `parseAs` value reads the body, for each member of a union; `undefined` reads JSON. */
144
199
  type ParseAsValue<Value> = Value extends ParseAs ? Value : "json";
@@ -178,10 +233,16 @@ type RequestBodyOption<T> =
178
233
  * gave a conditional type no `FetchOptions` argument was assignable to (TS2345). The operations
179
234
  * of a union are not merged into one deep init type, which gave TS2589 on hundreds of paths
180
235
  * (upstream #1882).
236
+ * Only the options that depend on the operation distribute; the others stay outside the
237
+ * conditional type. While `request()` has not fixed its method, TypeScript reads the init's
238
+ * contextual type through the constraints of the method and the path, and stops after ten nested
239
+ * instantiations of one conditional type: with every option inside it, `parseAs: "text"` had no
240
+ * contextual type, widened to `string`, and the init fell back to its constraint (`data` the JSON
241
+ * body). `FetchOptions<never>` is still `never`.
181
242
  */
182
- type FetchOptions<T> = T extends unknown
183
- ? RequestOptions<T> & Omit<RequestInit, "body" | "headers">
184
- : never;
243
+ type FetchOptions<T> = (T extends unknown ? OperationRequestOptions<T> : never) &
244
+ SharedRequestOptions &
245
+ Omit<RequestInit, "body" | "headers">;
185
246
 
186
247
  // Readable<T> strips $Write markers (writeOnly properties excluded from response)
187
248
  type FetchResponse<
@@ -201,18 +262,25 @@ type FetchResponse<
201
262
  response: Response;
202
263
  };
203
264
 
204
- type RequestOptions<T> = ParamsOption<T> &
265
+ type RequestOptions<T> = OperationRequestOptions<T> & SharedRequestOptions;
266
+
267
+ /** The options of a call that depend on its operation `T`. */
268
+ type OperationRequestOptions<T> = ParamsOption<T> &
205
269
  RequestBodyOption<T> & {
206
- baseUrl?: string | undefined;
207
270
  querySerializer?: QuerySerializer<T> | QuerySerializerOptions | undefined;
208
271
  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
272
  };
215
273
 
274
+ /** The options of a call that are the same for every operation. */
275
+ type SharedRequestOptions = {
276
+ baseUrl?: string | undefined;
277
+ pathSerializer?: PathSerializer | undefined;
278
+ parseAs?: ParseAs | undefined;
279
+ fetch?: ClientOptions["fetch"];
280
+ headers?: HeadersOptions | undefined;
281
+ middleware?: Middleware[] | undefined;
282
+ };
283
+
216
284
  type MergedOptions<T = unknown> = {
217
285
  baseUrl: string;
218
286
  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.12",
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.12",
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,26 +441,59 @@ 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
- /** Return first JSON-like 2XX response from a path + HTTP method */
457
+ /**
458
+ * Return first JSON-like 2XX response from a path + HTTP method; for a union of operations, the
459
+ * union of each one's.
460
+ */
430
461
  // biome-ignore lint/suspicious/noExplicitAny: unknown rejects the generated interfaces
431
- export type SuccessResponseJSON<PathMethod extends Record<string | number, any>> = SuccessResponse<
432
- ResponseObjectMap<PathMethod>,
433
- JSONMediaType
434
- >;
462
+ export type SuccessResponseJSON<PathMethod extends Record<string | number, any>> =
463
+ PathMethod extends unknown
464
+ ? SuccessResponse<ResponseObjectMap<PathMethod>, JSONMediaKeys<ResponseObjectMap<PathMethod>>>
465
+ : never;
435
466
 
436
- /** Return JSON-like 1XX, 3XX, 4XX, 5XX and `default` responses from a path + HTTP method */
467
+ /**
468
+ * Return JSON-like 1XX, 3XX, 4XX, 5XX and `default` responses from a path + HTTP method; for a
469
+ * union of operations, the union of each one's.
470
+ */
437
471
  // biome-ignore lint/suspicious/noExplicitAny: unknown rejects the generated interfaces
438
- export type ErrorResponseJSON<PathMethod extends Record<string | number, any>> = ErrorResponse<
439
- ResponseObjectMap<PathMethod>,
440
- JSONMediaType
441
- >;
472
+ export type ErrorResponseJSON<PathMethod extends Record<string | number, any>> =
473
+ PathMethod extends unknown
474
+ ? ErrorResponse<ResponseObjectMap<PathMethod>, JSONMediaKeys<ResponseObjectMap<PathMethod>>>
475
+ : never;
476
+
477
+ /**
478
+ * The media types of the responses `T` (of one operation) that may hold JSON (`IsJSONMediaType`),
479
+ * for the JSON helpers to pick by: their keys, as `JSONMediaType` cannot leave out a `json` in
480
+ * parameters. A content map with an index signature has no such key, so it gives
481
+ * `JSONMediaType`, which picks the schema of its signature. The helpers pick per operation: the
482
+ * keys of a union of response maps are only the status codes they share, so the media types of
483
+ * the other statuses were lost, and the media types of one operation could pick in another.
484
+ */
485
+ type JSONMediaKeys<T> = {
486
+ [Status in keyof T]: T[Status] extends { content: infer Content }
487
+ ? { [Key in keyof Content]: JSONMediaKey<Key> }[keyof Content]
488
+ : never;
489
+ }[keyof T];
490
+
491
+ /** `Key` when it may hold JSON; `JSONMediaType` for the wide key of an index signature. */
492
+ type JSONMediaKey<Key> = string extends Key
493
+ ? JSONMediaType
494
+ : IsJSONMediaType<Key> extends true
495
+ ? Key
496
+ : never;
442
497
 
443
498
  /** Return JSON-like request body from a path + HTTP method */
444
499
  export type RequestBodyJSON<PathMethod> = JSONLike<
package/src/index.d.ts CHANGED
@@ -139,16 +139,71 @@ 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; each member of a union of options gives its own data.
147
+ * No `infer`: TypeScript relates a type to a deferred conditional type through both of its
148
+ * branches only when it infers nothing, and generic hooks return the data of a call as this type.
149
+ * It distributes over `Options`, as openapi-fetch's does, so the options of a generic call (a
150
+ * wrapper's `FetchOptions` of a generic path with `parseAs: "text"`) give the data of their
151
+ * constraint: TypeScript reads a deferred distributive type through its check type's constraint.
148
152
  */
149
- export type ParseAsResponse<T, Options> = Options extends { parseAs: ParseAs | undefined }
150
- ? BodyType<T>[ParseAsValue<Options["parseAs"]>]
151
- : T;
153
+ export type ParseAsResponse<T, Options> = Options extends unknown
154
+ ? BodyType<T>[ParseAsKey<Options>]
155
+ : never;
156
+
157
+ /**
158
+ * The key of `BodyType` that the options `Options` read the body with. A conditional type of its
159
+ * own, inside the index: one in the branch of `ParseAsResponse` that picked `T` would be read
160
+ * through both of its branches while TypeScript reads the constraint above, as `never` fits any
161
+ * check type, so a generic `parseAs: "text"` gave the JSON body too.
162
+ */
163
+ type ParseAsKey<Options> = [MissingParseAsValues<Options>] extends [never]
164
+ ? "json"
165
+ : ParseAsValue<ParseAsValues<Options>>;
166
+
167
+ /**
168
+ * The `parseAs` values that `Options` may have; `undefined` when it may be left out. One
169
+ * conditional type, not a union: `BodyType<T>` indexed by a union of generic keys is read as the
170
+ * intersection of their bodies when another type is checked against it. `any` options give `any`,
171
+ * so their data is `any`, as in openapi-fetch: a caller that passes an `any` init (as the generic
172
+ * hooks of openapi-typesafe-react-query do) returns it as the data type it declares. The branch
173
+ * is `any` itself, not `Options`: a type parameter there stops TypeScript relating the data of a
174
+ * generic call to a wrapper's (ADR-030).
175
+ */
176
+ type ParseAsValues<Options> = Options extends unknown
177
+ ? IsAny<Options> extends true
178
+ ? // biome-ignore lint/suspicious/noExplicitAny: any options give any data, as in openapi-fetch
179
+ any
180
+ :
181
+ | (Options extends RequiredParseAs ? never : undefined)
182
+ | { [Value in ParseAs]: Options extends WithoutParseAs<Value> ? never : Value }[ParseAs]
183
+ : never;
184
+
185
+ /**
186
+ * The `parseAs` values that the options `Options` (one member of a union) cannot have, as
187
+ * `ParseAsValues` reads them.
188
+ */
189
+ type MissingParseAsValues<Options> =
190
+ | ([Options] extends [RequiredParseAs] ? undefined : never)
191
+ | { [Value in ParseAs]: [Options] extends [WithoutParseAs<Value>] ? Value : never }[ParseAs];
192
+
193
+ /** Options that always have a `parseAs`. */
194
+ type RequiredParseAs = { parseAs: ParseAs };
195
+
196
+ /**
197
+ * Options that cannot have the `parseAs` value `Value`: `undefined` options, or options whose
198
+ * `parseAs` (if any) leaves it out. Each
199
+ * check takes `Options` as its checked type and a fixed type here, so TypeScript relates the
200
+ * data of a generic call to a wrapper's (ADR-030); one union of these types instead would let
201
+ * TypeScript split `parseAs?: ParseAs` into its values and match each one. The `object` keeps
202
+ * options without `parseAs` (`{ headers }`) from failing as a weak type.
203
+ */
204
+ type WithoutParseAs<Value extends ParseAs> =
205
+ | undefined
206
+ | (object & { parseAs?: Exclude<ParseAs, Value> | undefined });
152
207
 
153
208
  /** How a `parseAs` value reads the body, for each member of a union; `undefined` reads JSON. */
154
209
  type ParseAsValue<Value> = Value extends ParseAs ? Value : "json";
@@ -188,10 +243,16 @@ export type RequestBodyOption<T> =
188
243
  * gave a conditional type no `FetchOptions` argument was assignable to (TS2345). The operations
189
244
  * of a union are not merged into one deep init type, which gave TS2589 on hundreds of paths
190
245
  * (upstream #1882).
246
+ * Only the options that depend on the operation distribute; the others stay outside the
247
+ * conditional type. While `request()` has not fixed its method, TypeScript reads the init's
248
+ * contextual type through the constraints of the method and the path, and stops after ten nested
249
+ * instantiations of one conditional type: with every option inside it, `parseAs: "text"` had no
250
+ * contextual type, widened to `string`, and the init fell back to its constraint (`data` the JSON
251
+ * body). `FetchOptions<never>` is still `never`.
191
252
  */
192
- export type FetchOptions<T> = T extends unknown
193
- ? RequestOptions<T> & Omit<RequestInit, "body" | "headers">
194
- : never;
253
+ export type FetchOptions<T> = (T extends unknown ? OperationRequestOptions<T> : never) &
254
+ SharedRequestOptions &
255
+ Omit<RequestInit, "body" | "headers">;
195
256
 
196
257
  // Readable<T> strips $Write markers (writeOnly properties excluded from response)
197
258
  export type FetchResponse<
@@ -211,18 +272,25 @@ export type FetchResponse<
211
272
  response: Response;
212
273
  };
213
274
 
214
- export type RequestOptions<T> = ParamsOption<T> &
275
+ export type RequestOptions<T> = OperationRequestOptions<T> & SharedRequestOptions;
276
+
277
+ /** The options of a call that depend on its operation `T`. */
278
+ type OperationRequestOptions<T> = ParamsOption<T> &
215
279
  RequestBodyOption<T> & {
216
- baseUrl?: string | undefined;
217
280
  querySerializer?: QuerySerializer<T> | QuerySerializerOptions | undefined;
218
281
  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
282
  };
225
283
 
284
+ /** The options of a call that are the same for every operation. */
285
+ type SharedRequestOptions = {
286
+ baseUrl?: string | undefined;
287
+ pathSerializer?: PathSerializer | undefined;
288
+ parseAs?: ParseAs | undefined;
289
+ fetch?: ClientOptions["fetch"];
290
+ headers?: HeadersOptions | undefined;
291
+ middleware?: Middleware[] | undefined;
292
+ };
293
+
226
294
  export type MergedOptions<T = unknown> = {
227
295
  baseUrl: string;
228
296
  parseAs: ParseAs;