openapi-typesafe-fetch 1.0.0-rc.1 → 1.0.0-rc.2

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/dist/index.d.cts CHANGED
@@ -15,19 +15,23 @@ interface ClientOptions extends Omit<RequestInit, "headers"> {
15
15
  /** global pathSerializer */
16
16
  pathSerializer?: PathSerializer;
17
17
  headers?: HeadersOptions;
18
- /** RequestInit extension object to pass as 2nd argument to fetch when supported (defaults to undefined) */
18
+ /**
19
+ * RequestInit extension object to pass as 2nd argument to fetch when supported (defaults to
20
+ * undefined)
21
+ */
19
22
  requestInitExt?: Record<string, unknown>;
20
23
  }
21
24
 
22
25
  /**
23
26
  * `ClientOptions` for a Fetch API implementation with its own types, such as the `undici` package
24
- * (upstream #2562). TypeScript cannot relate its `Request` and `Response` to the DOM lib's (`duplex`,
25
- * `RequestDestination`, `BodyInit` differ), so only what the client needs is checked: a `fetch`
26
- * that returns a promise and a `Request` class that takes a URL. Pass the `fetch` and the `Request`
27
- * of the same implementation; middleware and results still use the DOM types. The `createClient`
28
- * overload with `ClientOptions` comes first, so an inline `fetch` gets a typed `Request` input.
27
+ * (upstream #2562). TypeScript cannot relate its `Request` and `Response` to the DOM lib's
28
+ * (`duplex`, `RequestDestination`, `BodyInit` differ), so only what the client needs is checked: a
29
+ * `fetch` that returns a promise and a `Request` class that takes a URL. Pass the `fetch` and the
30
+ * `Request` of the same implementation; middleware and results still use the DOM types. The
31
+ * `createClient` overload with `ClientOptions` comes first, so an inline `fetch` gets a typed
32
+ * `Request` input.
29
33
  */
30
- interface OtherFetchClientOptions extends Omit<ClientOptions, "fetch" | "Request"> {
34
+ interface CustomFetchClientOptions extends Omit<ClientOptions, "fetch" | "Request"> {
31
35
  /** `fetch` of another implementation (for example `fetch` from `undici`) */
32
36
  fetch?: (input: never, init?: never) => PromiseLike<object>;
33
37
  /** `Request` of the same implementation (for example `Request` from `undici`) */
@@ -42,19 +46,28 @@ type HeadersOptions =
42
46
  | Record<string, string | number | boolean | (string | number | boolean)[] | null | undefined>;
43
47
 
44
48
  type QuerySerializer<T> = (
45
- query: T extends { parameters: any } ? NonNullable<T["parameters"]["query"]> : Record<string, unknown>,
49
+ // biome-ignore lint/suspicious/noExplicitAny: T["parameters"]["query"] needs an any constraint
50
+ query: T extends { parameters: any }
51
+ ? NonNullable<T["parameters"]["query"]>
52
+ : Record<string, unknown>,
46
53
  ) => string;
47
54
 
48
55
  /** @see https://swagger.io/docs/specification/serialization/#query */
49
56
  type QuerySerializerOptions = {
50
- /** Set serialization for arrays. @see https://swagger.io/docs/specification/serialization/#query */
57
+ /**
58
+ * Set serialization for arrays.
59
+ * @see https://swagger.io/docs/specification/serialization/#query
60
+ */
51
61
  array?: {
52
62
  /** default: "form" */
53
63
  style: "form" | "spaceDelimited" | "pipeDelimited";
54
64
  /** default: true */
55
65
  explode: boolean;
56
66
  };
57
- /** Set serialization for objects. @see https://swagger.io/docs/specification/serialization/#query */
67
+ /**
68
+ * Set serialization for objects.
69
+ * @see https://swagger.io/docs/specification/serialization/#query
70
+ */
58
71
  object?: {
59
72
  /** default: "deepObject" */
60
73
  style: "form" | "deepObject";
@@ -71,6 +84,8 @@ type QuerySerializerOptions = {
71
84
  allowReserved?: boolean;
72
85
  };
73
86
 
87
+ // The result is the fetch body; openapi-fetch types it as `any` (ADR-030).
88
+ // biome-ignore lint/suspicious/noExplicitAny: openapi-fetch's type, kept for parity
74
89
  type BodySerializer<T> = (body: OperationRequestBodyContent<T>) => any;
75
90
 
76
91
  type PathSerializer = (pathname: string, pathParams: Record<string, unknown>) => string;
@@ -96,7 +111,7 @@ interface DefaultParamsOption {
96
111
  }
97
112
 
98
113
  type ParamsOption<T> = T extends {
99
- parameters: any;
114
+ parameters: unknown;
100
115
  }
101
116
  ? RequiredKeysOf<T["parameters"]> extends never
102
117
  ? { params?: T["parameters"] }
@@ -111,7 +126,8 @@ type IsAny<T> = 0 extends 1 & T ? true : false;
111
126
  // gave `{ body?: never } | { body: any }` and so a required init (upstream #2526).
112
127
  type RequestBodyOption<T> =
113
128
  IsAny<T> extends true
114
- ? { body?: any }
129
+ ? // biome-ignore lint/suspicious/noExplicitAny: an any schema takes any body (upstream #2526)
130
+ { body?: any }
115
131
  : Writable<OperationRequestBodyContent<T>> extends never
116
132
  ? { body?: never }
117
133
  : IsOperationRequestBodyOptional<T> extends true
@@ -121,7 +137,12 @@ type RequestBodyOption<T> =
121
137
  type FetchOptions<T> = RequestOptions<T> & Omit<RequestInit, "body" | "headers">;
122
138
 
123
139
  // Readable<T> strips $Write markers (writeOnly properties excluded from response)
124
- type FetchResponse<T extends Record<string | number, any>, Options, Media extends MediaType> =
140
+ type FetchResponse<
141
+ // biome-ignore lint/suspicious/noExplicitAny: unknown rejects the generated interfaces
142
+ T extends Record<string | number, any>,
143
+ Options,
144
+ Media extends MediaType,
145
+ > =
125
146
  | {
126
147
  data: ParseAsResponse<Readable<SuccessResponse<ResponseObjectMap<T>, Media>>, Options>;
127
148
  error?: never;
@@ -170,7 +191,7 @@ interface MiddlewareCallbackParams {
170
191
  readonly params: MiddlewareRequestParams;
171
192
  /** Unique ID for this request */
172
193
  readonly id: string;
173
- /** createClient options (read-only) */
194
+ /** The options of this request after merging the client and request options (read-only) */
174
195
  readonly options: MergedOptions;
175
196
  }
176
197
 
@@ -201,17 +222,30 @@ type Middleware =
201
222
  onError: MiddlewareOnError;
202
223
  };
203
224
 
204
- /** The init of one operation, `undefined` too when nothing in it is required. */
205
- type OperationInit<Operation> =
206
- RequiredKeysOf<FetchOptions<Operation>> extends never ? FetchOptions<Operation> | undefined : FetchOptions<Operation>;
207
-
208
225
  /**
209
226
  * This type helper makes the 2nd function param required if params/requestBody are required;
210
227
  * otherwise, optional. `Location` is any key: with `Paths extends {}` a method is not known to be a
211
228
  * key of `Paths[Path]` (upstream #2267). For a union of operations, the union of their inits: one
212
229
  * deep type of every init gave TS2589 on hundreds of paths (upstream #1882).
213
230
  */
214
- type MaybeOptionalInit<Params, Location extends PropertyKey> = OperationInits<FilterKeys<Params, Location>>;
231
+ type MaybeOptionalInit<Params, Location extends PropertyKey> = OperationInits<
232
+ FilterKeys<Params, Location>
233
+ >;
234
+
235
+ /**
236
+ * The inits of the operations, and `undefined` only when none of them requires anything. One
237
+ * operation's init is `undefined` too when nothing in it is required, as in openapi-fetch. For a
238
+ * union of operations (a union of paths, or `Parameters<typeof client.request>`), `undefined`
239
+ * would make the whole init optional, although a call to one of the paths sends the init it gets,
240
+ * and a path parameter left out stays `{id}` in the URL.
241
+ * One conditional type, as openapi-fetch's, not a union with a conditional `undefined`: while the
242
+ * operation is generic, a union made `FetchResponse` distribute over its members, so the `data` of
243
+ * a generic wrapper's call no longer related to `FetchResponse<Paths[Path][Method], Init, Media>`.
244
+ */
245
+ type OperationInits<Operation> =
246
+ RequiredKeysOf<OperationFetchOptions<Operation>> extends never
247
+ ? OperationFetchOptions<Operation> | undefined
248
+ : OperationFetchOptions<Operation>;
215
249
 
216
250
  /**
217
251
  * Each operation's init. A distributive alias, not `extends infer Operation`: while the path is
@@ -219,31 +253,27 @@ type MaybeOptionalInit<Params, Location extends PropertyKey> = OperationInits<Fi
219
253
  * contextual type of an argument and a literal such as `"available"` widened to `string`
220
254
  * (swr-openapi's `Exact<Init>` checks).
221
255
  */
222
- type OperationInits<Operation> = Operation extends unknown ? OperationInit<Operation> : never;
223
-
224
- /** For each init of a union, whether it may be left out: nothing in it is required. */
225
- type InitOptionality<Init> = Init extends unknown ? (RequiredKeysOf<Init> extends never ? true : false) : never;
226
-
227
- /**
228
- * `true` when the init may be left out: nothing in it is required. For a union of inits (a union
229
- * of paths, or `Parameters<typeof client.request>`), only when each of them may be left out: a
230
- * call to one of the paths sends the init it gets, and a path parameter left out stays `{id}` in
231
- * the URL.
232
- */
233
- type IsInitOptional<Init> = InitOptionality<Init> extends true ? true : false;
256
+ type OperationFetchOptions<Operation> = Operation extends unknown ? FetchOptions<Operation> : never;
234
257
 
235
258
  // The final init param to accept.
236
259
  // - Determines if the param is optional or not.
237
260
  // - Performs arbitrary [key: string] addition.
238
- // Note: the addition MUST happen after all the inference happens (otherwise TS can’t infer if init is required or not).
239
- // - Reads `InitOptionality` and names the addition: through `IsInitOptional`, or with the addition
240
- // written out in both branches, TypeScript gave `never` for `Parameters` of the methods.
241
- type InitParam<Init> = InitOptionality<Init> extends true ? [WithAnyKeys<Init>?] : [WithAnyKeys<Init>];
261
+ // Note: the addition MUST happen after all the inference happens (otherwise TS can’t infer if
262
+ // init is required or not).
263
+ // - The condition is openapi-fetch's. TypeScript relates two conditional types only when their
264
+ // `extends` types are the same and their check types relate, so a client type written against
265
+ // openapi-fetch's `InitParam` (a wrapper, a cast) accepted this client only with this condition.
266
+ // A union of inits is required through `MaybeOptionalInit`, which then leaves out `undefined`.
267
+ // - The addition is a named alias: written out in both branches, TypeScript gave `never` for
268
+ // `Parameters` of the methods.
269
+ type InitParam<Init> =
270
+ RequiredKeysOf<Init> extends never ? [WithAnyKeys<Init>?] : [WithAnyKeys<Init>];
242
271
 
243
272
  /** The init with any other key (`[key: string]: unknown`), added after inference. */
244
273
  type WithAnyKeys<Init> = Init & { [key: string]: unknown };
245
274
 
246
275
  /**
276
+ * @internal Exported for openapi-typesafe-react-query only; not part of the API (ADR-034).
247
277
  * The operation of `Method` on `Path`. Not `Paths[Path][Method]`: `Paths` is only `{}`, because
248
278
  * the generated `interface paths` has no index signature and failed a `Record<string, …>`
249
279
  * constraint with TS2344 (upstream #2267), so TypeScript does not know `Method` is a key. For a
@@ -252,6 +282,7 @@ type WithAnyKeys<Init> = Init & { [key: string]: unknown };
252
282
  */
253
283
  type PathOperation<Paths, Path, Method> = Extract<
254
284
  FilterKeys<Paths[Path & keyof Paths], Method>,
285
+ // biome-ignore lint/suspicious/noExplicitAny: unknown rejects the generated interfaces
255
286
  Record<string | number, any>
256
287
  >;
257
288
 
@@ -274,8 +305,18 @@ type ClientRequestMethod<Paths extends {}, Media extends MediaType> = <
274
305
  ) => Promise<FetchResponse<PathOperation<Paths, Path, Method>, Init, Media>>;
275
306
 
276
307
  // Only HTTP method keys become calls: `parameters` and `additionalOperations` are not methods.
277
- type ClientForPath<PathInfo extends Record<string | number, any>, Media extends MediaType> = {
278
- [Method in keyof PathInfo as Method extends HttpMethod ? Uppercase<Method> : never]: <
308
+ // They are left out by `Pick`, which keeps the `?` of a method, not by the key remapping: that is
309
+ // openapi-fetch's, and TypeScript relates two generic mapped types only when their key remappings
310
+ // are the same, so a path-based client type written like openapi-fetch's failed a cast (TS2352).
311
+ type ClientForPath<
312
+ // biome-ignore lint/suspicious/noExplicitAny: unknown rejects the generated interfaces
313
+ PathInfo extends Record<string | number, any>,
314
+ Media extends MediaType,
315
+ > = PathMethodCalls<Pick<PathInfo, HttpMethod & keyof PathInfo>, Media>;
316
+
317
+ // biome-ignore lint/suspicious/noExplicitAny: unknown rejects the generated interfaces
318
+ type PathMethodCalls<PathInfo extends Record<string | number, any>, Media extends MediaType> = {
319
+ [Method in keyof PathInfo as Uppercase<string & Method>]: <
279
320
  Init extends MaybeOptionalInit<PathInfo, Method>,
280
321
  >(
281
322
  ...init: InitParam<Init>
@@ -308,16 +349,25 @@ interface Client<Paths extends {}, Media extends MediaType = MediaType> {
308
349
  eject(...middleware: Middleware[]): void;
309
350
  }
310
351
 
311
- type ClientPathsWithMethod<CreatedClient extends Client<any, any>, Method extends HttpMethod> =
352
+ type ClientPathsWithMethod<
353
+ // biome-ignore lint/suspicious/noExplicitAny: matches a client of any paths, as in openapi-fetch
354
+ CreatedClient extends Client<any, any>,
355
+ Method extends HttpMethod,
356
+ > =
312
357
  CreatedClient extends Client<infer Paths, infer _Media> ? PathsWithMethod<Paths, Method> : never;
313
358
 
314
359
  type MethodResponse<
360
+ // biome-ignore lint/suspicious/noExplicitAny: matches a client of any paths, as in openapi-fetch
315
361
  CreatedClient extends Client<any, any>,
316
362
  Method extends HttpMethod,
317
363
  Path extends ClientPathsWithMethod<CreatedClient, Method>,
318
364
  Options = {},
319
365
  > =
320
- CreatedClient extends Client<infer Paths extends { [key: string]: any }, infer Media extends MediaType>
366
+ CreatedClient extends Client<
367
+ // biome-ignore lint/suspicious/noExplicitAny: unknown rejects the generated interfaces
368
+ infer Paths extends { [key: string]: any },
369
+ infer Media extends MediaType
370
+ >
321
371
  ? NonNullable<FetchResponse<Paths[Path][Method], Options, Media>["data"]>
322
372
  : never;
323
373
 
@@ -325,23 +375,30 @@ declare function createClient<Paths extends {}, Media extends MediaType = MediaT
325
375
  clientOptions?: ClientOptions,
326
376
  ): Client<Paths, Media>;
327
377
  declare function createClient<Paths extends {}, Media extends MediaType = MediaType>(
328
- clientOptions?: OtherFetchClientOptions,
378
+ clientOptions?: CustomFetchClientOptions,
329
379
  ): Client<Paths, Media>;
330
380
 
331
- type PathBasedClient<Paths extends Record<string | number, any>, Media extends MediaType = MediaType> = {
381
+ type PathBasedClient<
382
+ // biome-ignore lint/suspicious/noExplicitAny: unknown rejects the generated interfaces
383
+ Paths extends Record<string | number, any>,
384
+ Media extends MediaType = MediaType,
385
+ > = {
332
386
  [Path in keyof Paths]: ClientForPath<Paths[Path], Media>;
333
387
  };
334
388
 
335
- declare function wrapAsPathBasedClient<Paths extends {}, Media extends MediaType = MediaType>(
336
- client: Client<Paths, Media>,
337
- ): PathBasedClient<Paths, Media>;
389
+ declare function wrapAsPathBasedClient<
390
+ Paths extends {},
391
+ Media extends MediaType = MediaType,
392
+ >(client: Client<Paths, Media>): PathBasedClient<Paths, Media>;
338
393
 
339
- declare function createPathBasedClient<Paths extends {}, Media extends MediaType = MediaType>(
340
- clientOptions?: ClientOptions,
341
- ): PathBasedClient<Paths, Media>;
342
- declare function createPathBasedClient<Paths extends {}, Media extends MediaType = MediaType>(
343
- clientOptions?: OtherFetchClientOptions,
344
- ): PathBasedClient<Paths, Media>;
394
+ declare function createPathBasedClient<
395
+ Paths extends {},
396
+ Media extends MediaType = MediaType,
397
+ >(clientOptions?: ClientOptions): PathBasedClient<Paths, Media>;
398
+ declare function createPathBasedClient<
399
+ Paths extends {},
400
+ Media extends MediaType = MediaType,
401
+ >(clientOptions?: CustomFetchClientOptions): PathBasedClient<Paths, Media>;
345
402
 
346
403
  /** Serialize primitive params to string */
347
404
  declare function serializePrimitiveParam(
@@ -382,12 +439,85 @@ declare function createQuerySerializer<T = unknown>(
382
439
  * @type {import("./index.js").defaultPathSerializer}
383
440
  * @see https://swagger.io/docs/specification/serialization/#path
384
441
  */
385
- declare function defaultPathSerializer(pathname: string, pathParams: Record<string, unknown>): string;
442
+ declare function defaultPathSerializer(
443
+ pathname: string,
444
+ pathParams: Record<string, unknown>,
445
+ ): string;
446
+
447
+ /** Bodies that `fetch` sends as they are, so `defaultBodySerializer` returns them unchanged. */
448
+ type RawRequestBody = FormData | Blob | URLSearchParams | ArrayBuffer | ArrayBufferView;
449
+
450
+ /**
451
+ * The result of `defaultBodySerializer` for a body of type `Body`: the body itself when it is raw,
452
+ * `undefined` for `undefined` (`JSON.stringify` gives it back), `ObjectResult` for an object, and a
453
+ * string otherwise. `unknown` (and `any`) may be any of these.
454
+ */
455
+ type SerializedBody<Body, ObjectResult> = unknown extends Body
456
+ ? RawRequestBody | string | FormData | undefined
457
+ : Body extends RawRequestBody
458
+ ? Body
459
+ : Body extends undefined
460
+ ? undefined
461
+ : Body extends object
462
+ ? ObjectResult
463
+ : string;
464
+
465
+ /** The `Content-Type` values that make `defaultBodySerializer` turn an object into `FormData`. */
466
+ type MultipartMediaType = "multipart/form-data" | `multipart/form-data;${string}`;
467
+
468
+ /** `Headers` drops the spaces and tabs in front of a value before the media type is read. */
469
+ type TrimmedStart<Text extends string> = Text extends ` ${infer Rest}` | `\t${infer Rest}`
470
+ ? TrimmedStart<Rest>
471
+ : Text;
472
+
473
+ /**
474
+ * The `Content-Type` of headers written as a record, in any letter case of the name: `never` when
475
+ * the record has none, `unknown` when the type does not tell (`Headers`, pairs, a string record).
476
+ */
477
+ type ContentTypeOf<GivenHeaders> = GivenHeaders extends undefined
478
+ ? never
479
+ : GivenHeaders extends readonly unknown[] | { get(name: string): unknown }
480
+ ? unknown
481
+ : string extends keyof GivenHeaders
482
+ ? unknown
483
+ : ContentTypeEntries<GivenHeaders>[keyof GivenHeaders];
484
+
485
+ /** For each entry of a header record: its value when it is the `Content-Type`, else `never`. */
486
+ type ContentTypeEntries<Record> = { [Name in keyof Record]: ContentTypeEntry<Name, Record[Name]> };
487
+
488
+ /** The value of a header record entry when its name is `Content-Type` in any letter case. */
489
+ type ContentTypeEntry<Name, Value> =
490
+ Lowercase<Name & string> extends "content-type" ? Value : never;
491
+
492
+ /**
493
+ * What an object becomes with a `Content-Type` of type `ContentType`: `FormData` for a multipart
494
+ * one, a string for any other one or for none, and either when the type is not a literal.
495
+ */
496
+ type ObjectBodyResult<ContentType> = [ContentType] extends [never]
497
+ ? string
498
+ : ContentType extends null | undefined
499
+ ? string
500
+ : ContentType extends string
501
+ ? string extends ContentType
502
+ ? string | FormData
503
+ : Lowercase<TrimmedStart<ContentType>> extends MultipartMediaType
504
+ ? FormData
505
+ : string
506
+ : string | FormData;
386
507
 
387
508
  /**
388
- * Serialize body object to string; with a form content type in `headers`, as URL-encoded form data.
509
+ * Serialize a request body. A raw body (`FormData`, `Blob`, `URLSearchParams`, `ArrayBuffer`, a
510
+ * typed array) is returned as it is. With `headers`: a form `Content-Type` gives URL-encoded form
511
+ * data, a `multipart/form-data` one turns an object into `FormData`, and another non-JSON one keeps
512
+ * a string as it is. Anything else is JSON. Without `headers` an object can only become JSON.
513
+ * Headers written as a record literal tell the result type of an object body: `FormData` for a
514
+ * multipart `Content-Type`, `string` for another one or none. Other headers: `string | FormData`.
389
515
  */
390
- declare function defaultBodySerializer<T>(body: T, headers?: HeadersOptions): string;
516
+ declare function defaultBodySerializer<T>(body: T): SerializedBody<T, string>;
517
+ declare function defaultBodySerializer<
518
+ T,
519
+ const GivenHeaders extends HeadersOptions | undefined,
520
+ >(body: T, headers: GivenHeaders): SerializedBody<T, ObjectBodyResult<ContentTypeOf<GivenHeaders>>>;
391
521
 
392
522
  /** Construct URL string from baseUrl and handle path and query params */
393
523
  declare function createFinalURL<O>(
@@ -410,4 +540,4 @@ declare function mergeHeaders(...allHeaders: (HeadersOptions | undefined)[]): He
410
540
  declare function removeTrailingSlash(url: string): string;
411
541
 
412
542
  export { createFinalURL, createPathBasedClient, createQuerySerializer, createClient as default, defaultBodySerializer, defaultPathSerializer, mergeHeaders, removeTrailingSlash, serializeArrayParam, serializeObjectParam, serializePrimitiveParam, wrapAsPathBasedClient };
413
- export type { BodySerializer, Client, ClientForPath, ClientMethod, ClientOptions, ClientPathsWithMethod, ClientRequestMethod, DefaultParamsOption, FetchOptions, FetchResponse, HeadersOptions, IsInitOptional, MaybeOptionalInit, MergedOptions, MethodResponse, Middleware, MiddlewareCallbackParams, MiddlewareRequestParams, OtherFetchClientOptions, ParamsOption, ParseAs, ParseAsResponse, PathBasedClient, PathOperation, PathSerializer, QuerySerializer, QuerySerializerOptions, RequestBodyOption, RequestOptions };
543
+ export type { BodySerializer, Client, ClientForPath, ClientMethod, ClientOptions, ClientPathsWithMethod, ClientRequestMethod, CustomFetchClientOptions, DefaultParamsOption, FetchOptions, FetchResponse, HeadersOptions, MaybeOptionalInit, MergedOptions, MethodResponse, Middleware, MiddlewareCallbackParams, MiddlewareRequestParams, ParamsOption, ParseAs, ParseAsResponse, PathBasedClient, PathOperation, PathSerializer, QuerySerializer, QuerySerializerOptions, RequestBodyOption, RequestOptions };