diff --git a/README.md b/README.md
index ff610bf4..5985f871 100644
--- a/README.md
+++ b/README.md
@@ -1,388 +1,402 @@
-# ofetch
-
-
-
-[](https://npmjs.com/package/ofetch)
-[](https://npm.chart.dev/ofetch)
-
-
-
-A better fetch API. Works on node, browser, and workers.
-
-> [!IMPORTANT]
-> You are on v2 (alpha) development branch. See [v1](https://github.com/unjs/ofetch/tree/v1) for v1 docs.
-
-
- Spoiler
-
-
-
-## 🚀 Quick Start
-
-Install:
-
-```bash
-npx nypm i ofetch
-```
-
-Import:
-
-```js
-import { ofetch } from "ofetch";
-```
-
-## ✔️ Parsing Response
-
-`ofetch` smartly parse JSON responses.
-
-```js
-const { users } = await ofetch("/api/users");
-```
-
-For binary content types, `ofetch` will instead return a `Blob` object.
-
-You can optionally provide a different parser than `JSON.parse`, or specify `blob`, `arrayBuffer`, `text` or `stream` to force parsing the body with the respective `FetchResponse` method.
-
-```js
-// Return text as is
-await ofetch("/movie?lang=en", { parseResponse: (txt) => txt });
-
-// Get the blob version of the response
-await ofetch("/api/generate-image", { responseType: "blob" });
-
-// Get the stream version of the response
-await ofetch("/api/generate-image", { responseType: "stream" });
-```
-
-## ✔️ JSON Body
-
-If an object or a class with a `.toJSON()` method is passed to the `body` option, `ofetch` automatically stringifies it.
-
-`ofetch` utilizes `JSON.stringify()` to convert the passed object. Classes without a `.toJSON()` method have to be converted into a string value in advance before being passed to the `body` option.
-
-For `PUT`, `PATCH`, and `POST` request methods, when a string or object body is set, `ofetch` adds the default `"content-type": "application/json"` and `accept: "application/json"` headers (which you can always override).
-
-Additionally, `ofetch` supports binary responses with `Buffer`, `ReadableStream`, `Stream`, and [compatible body types](https://developer.mozilla.org/en-US/docs/Web/API/fetch#body). `ofetch` will automatically set the `duplex: "half"` option for streaming support!
-
-**Example:**
-
-```js
-const { users } = await ofetch("/api/users", {
- method: "POST",
- body: { some: "json" },
-});
-```
-
-## ✔️ Handling Errors
-
-`ofetch` Automatically throws errors when `response.ok` is `false` with a friendly error message and compact stack (hiding internals).
-
-A parsed error body is available with `error.data`. You may also use `FetchError` type.
-
-```ts
-await ofetch("https://google.com/404");
-// FetchError: [GET] "https://google/404": 404 Not Found
-// at async main (/project/playground.ts:4:3)
-```
-
-To catch error response:
-
-```ts
-await ofetch("/url").catch((error) => error.data);
-```
-
-To bypass status error catching you can set `ignoreResponseError` option:
-
-```ts
-await ofetch("/url", { ignoreResponseError: true });
-```
-
-## ✔️ Auto Retry
-
-`ofetch` Automatically retries the request if an error happens and if the response status code is included in `retryStatusCodes` list:
-
-**Retry status codes:**
-
-- `408` - Request Timeout
-- `409` - Conflict
-- `425` - Too Early ([Experimental](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Early-Data))
-- `429` - Too Many Requests
-- `500` - Internal Server Error
-- `502` - Bad Gateway
-- `503` - Service Unavailable
-- `504` - Gateway Timeout
-
-You can specify the amount of retry and delay between them using `retry` and `retryDelay` options and also pass a custom array of codes using `retryStatusCodes` option.
-
-The default for `retry` is `1` retry, except for `POST`, `PUT`, `PATCH`, and `DELETE` methods where `ofetch` does not retry by default to avoid introducing side effects. If you set a custom value for `retry` it will **always retry** for all requests.
-
-The default for `retryDelay` is `0` ms.
-
-```ts
-await ofetch("http://google.com/404", {
- retry: 3,
- retryDelay: 500, // ms
- retryStatusCodes: [404, 500], // response status codes to retry
-});
-```
-
-## ✔️ Timeout
-
-You can specify `timeout` in milliseconds to automatically abort a request after a timeout (default is disabled).
-
-```ts
-await ofetch("http://google.com/404", {
- timeout: 3000, // Timeout after 3 seconds
-});
-```
-
-## ✔️ Type Friendly
-
-The response can be type assisted:
-
-```ts
-const article = await ofetch(`/api/article/${id}`);
-// Auto complete working with article.id
-```
-
-## ✔️ Adding `baseURL`
-
-By using `baseURL` option, `ofetch` prepends it for trailing/leading slashes and query search params for baseURL using [ufo](https://github.com/unjs/ufo):
-
-```js
-await ofetch("/config", { baseURL });
-```
-
-## ✔️ Adding Query Search Params
-
-By using `query` option (or `params` as alias), `ofetch` adds query search params to the URL by preserving the query in the request itself using [ufo](https://github.com/unjs/ufo):
-
-```js
-await ofetch("/movie?lang=en", { query: { id: 123 } });
-```
-
-## ✔️ Interceptors
-
-Providing async interceptors to hook into lifecycle events of `ofetch` call is possible.
-
-You might want to use `ofetch.create` to set shared interceptors.
-
-### `onRequest({ request, options })`
-
-`onRequest` is called as soon as `ofetch` is called, allowing you to modify options or do simple logging.
-
-```js
-await ofetch("/api", {
- async onRequest({ request, options }) {
- // Log request
- console.log("[fetch request]", request, options);
-
- // Add `?t=1640125211170` to query search params
- options.query = options.query || {};
- options.query.t = new Date();
- },
-});
-```
-
-### `onRequestError({ request, options, error })`
-
-`onRequestError` will be called when the fetch request fails.
-
-```js
-await ofetch("/api", {
- async onRequestError({ request, options, error }) {
- // Log error
- console.log("[fetch request error]", request, error);
- },
-});
-```
-
-### `onResponse({ request, options, response })`
-
-`onResponse` will be called after `fetch` call and parsing body.
-
-```js
-await ofetch("/api", {
- async onResponse({ request, response, options }) {
- // Log response
- console.log("[fetch response]", request, response.status, response.body);
- },
-});
-```
-
-### `onResponseError({ request, options, response })`
-
-`onResponseError` is the same as `onResponse` but will be called when fetch happens but `response.ok` is not `true`.
-
-```js
-await ofetch("/api", {
- async onResponseError({ request, response, options }) {
- // Log error
- console.log(
- "[fetch response error]",
- request,
- response.status,
- response.body
- );
- },
-});
-```
-
-### Passing array of interceptors
-
-If necessary, it's also possible to pass an array of function that will be called sequentially.
-
-```js
-await ofetch("/api", {
- onRequest: [
- () => {
- /* Do something */
- },
- () => {
- /* Do something else */
- },
- ],
-});
-```
-
-## ✔️ Create fetch with default options
-
-This utility is useful if you need to use common options across several fetch calls.
-
-**Note:** Defaults will be cloned at one level and inherited. Be careful about nested options like `headers`.
-
-```js
-const apiFetch = ofetch.create({ baseURL: "/api" });
-
-apiFetch("/test"); // Same as ofetch('/test', { baseURL: '/api' })
-```
-
-## 💡 Adding headers
-
-By using `headers` option, `ofetch` adds extra headers in addition to the request default headers:
-
-```js
-await ofetch("/movies", {
- headers: {
- Accept: "application/json",
- "Cache-Control": "no-cache",
- },
-});
-```
-
-## 🍣 Access to Raw Response
-
-If you need to access raw response (for headers, etc), you can use `ofetch.raw`:
-
-```js
-const response = await ofetch.raw("/sushi");
-
-// response._data
-// response.headers
-// ...
-```
-
-## 🌿 Using Native Fetch
-
-As a shortcut, you can use `ofetch.native` that provides native `fetch` API
-
-```js
-const json = await ofetch.native("/sushi").then((r) => r.json());
-```
-
-## 📡 SSE
-
-**Example:** Handle SSE response:
-
-```js
-const stream = await ofetch("/sse");
-const reader = stream.getReader();
-const decoder = new TextDecoder();
-while (true) {
- const { done, value } = await reader.read();
- if (done) break;
- // Here is the chunked text of the SSE response.
- const text = decoder.decode(value);
-}
-```
-
-## 🕵️ Proxy Support
-
-> [!IMPORTANT]
-> **Environment Variables:** Bun and Deno respect `HTTP_PROXY` and `HTTPS_PROXY` environment variables. Node.js requires setting `NODE_USE_ENV_PROXY=1` to enable [built-in proxy support](https://nodejs.org/api/http.html#http_built_in_proxy_support).
-
-### Node.js
-
-In Node.js (>= 18), you can use the `dispatcher` option with [undici](https://undici.nodejs.org/)'s `ProxyAgent`.
-
-```ts
-import { ProxyAgent } from "undici";
-
-const proxyAgent = new ProxyAgent("http://localhost:3128");
-
-await ofetch("https://icanhazip.com", { dispatcher: proxyAgent });
-```
-
-**Example:** Set proxy globally for all requests:
-
-```ts
-import { ProxyAgent, setGlobalDispatcher } from "undici";
-
-setGlobalDispatcher(new ProxyAgent("http://localhost:3128"));
-```
-
-**Example:** Allow self-signed certificates (USE AT YOUR OWN RISK!)
-
-```ts
-import { Agent } from "undici";
-
-// Note: This makes fetch unsecure against MITM attacks. USE AT YOUR OWN RISK!
-const unsecureAgent = new Agent({ connect: { rejectUnauthorized: false } });
-await ofetch("https://self-signed.example.com/", { dispatcher: unsecureAgent });
-```
-
-### Bun and Deno
-
-**Bun** supports the `proxy` option:
-
-```ts
-await ofetch("https://icanhazip.com", {
- proxy: "http://localhost:3128",
-});
-```
-
-**Deno** can also use undici with npm specifiers for programmatic configuration.
-
-### 💪 Augment `FetchOptions` interface
-
-You can augment the `FetchOptions` interface to add custom properties.
-
-```ts
-// Place this in any `.ts` or `.d.ts` file.
-// Ensure it's included in the project's tsconfig.json "files".
-declare module "ofetch" {
- interface FetchOptions {
- // Custom properties
- requiresAuth?: boolean;
- }
-}
-
-export {};
-```
-
-This lets you pass and use those properties with full type safety throughout `ofetch` calls.
-
-```ts
-const myFetch = ofetch.create({
- onRequest(context) {
- // ^? { ..., options: {..., requiresAuth?: boolean }}
- console.log(context.options.requiresAuth);
- },
-});
-
-myFetch("/foo", { requiresAuth: true });
-```
-
-## License
-
-💛 Published under the [MIT](https://github.com/h3js/rou3/blob/main/LICENSE) license.
+# ofetch
+
+
+
+[](https://npmjs.com/package/ofetch)
+[](https://npm.chart.dev/ofetch)
+
+
+
+A better fetch API. Works on node, browser, and workers.
+
+> [!IMPORTANT]
+> You are on v2 (alpha) development branch. See [v1](https://github.com/unjs/ofetch/tree/v1) for v1 docs.
+
+
+ Spoiler
+
+
+
+## 🚀 Quick Start
+
+Install:
+
+```bash
+npx nypm i ofetch
+```
+
+Import:
+
+```js
+import { ofetch } from "ofetch";
+```
+
+## ✔️ Parsing Response
+
+`ofetch` smartly parse JSON responses.
+
+```js
+const { users } = await ofetch("/api/users");
+```
+
+For binary content types, `ofetch` will instead return a `Blob` object.
+
+You can optionally provide a different parser than `JSON.parse`, or specify `blob`, `arrayBuffer`, `text` or `stream` to force parsing the body with the respective `FetchResponse` method.
+
+```js
+// Return text as is
+await ofetch("/movie?lang=en", { parseResponse: (txt) => txt });
+
+// Get the blob version of the response
+await ofetch("/api/generate-image", { responseType: "blob" });
+
+// Get the stream version of the response
+await ofetch("/api/generate-image", { responseType: "stream" });
+```
+
+## ✔️ JSON Body
+
+If an object or a class with a `.toJSON()` method is passed to the `body` option, `ofetch` automatically stringifies it.
+
+`ofetch` utilizes `JSON.stringify()` to convert the passed object. Classes without a `.toJSON()` method have to be converted into a string value in advance before being passed to the `body` option.
+
+For `PUT`, `PATCH`, and `POST` request methods, when a string or object body is set, `ofetch` adds the default `"content-type": "application/json"` and `accept: "application/json"` headers (which you can always override).
+
+Additionally, `ofetch` supports binary responses with `Buffer`, `ReadableStream`, `Stream`, and [compatible body types](https://developer.mozilla.org/en-US/docs/Web/API/fetch#body). `ofetch` will automatically set the `duplex: "half"` option for streaming support!
+
+**Example:**
+
+```js
+const { users } = await ofetch("/api/users", {
+ method: "POST",
+ body: { some: "json" },
+});
+```
+
+## ✔️ Handling Errors
+
+`ofetch` Automatically throws errors when `response.ok` is `false` with a friendly error message and compact stack (hiding internals).
+
+A parsed error body is available with `error.data`. You may also use `FetchError` type.
+
+```ts
+await ofetch("https://google.com/404");
+// FetchError: [GET] "https://google/404": 404 Not Found
+// at async main (/project/playground.ts:4:3)
+```
+
+To catch error response:
+
+```ts
+await ofetch("/url").catch((error) => error.data);
+```
+
+To bypass status error catching you can set `ignoreResponseError` option:
+
+```ts
+await ofetch("/url", { ignoreResponseError: true });
+```
+
+## ✔️ Auto Retry
+
+`ofetch` Automatically retries the request if an error happens and if the response status code is included in `retryStatusCodes` list:
+
+**Retry status codes:**
+
+- `408` - Request Timeout
+- `409` - Conflict
+- `425` - Too Early ([Experimental](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Early-Data))
+- `429` - Too Many Requests
+- `500` - Internal Server Error
+- `502` - Bad Gateway
+- `503` - Service Unavailable
+- `504` - Gateway Timeout
+
+You can specify the amount of retry and delay between them using `retry` and `retryDelay` options and also pass a custom array of codes using `retryStatusCodes` option.
+
+The default for `retry` is `1` retry, except for `POST`, `PUT`, `PATCH`, and `DELETE` methods where `ofetch` does not retry by default to avoid introducing side effects. If you set a custom value for `retry` it will **always retry** for all requests.
+
+The default for `retryDelay` is `0` ms.
+
+```ts
+await ofetch("http://google.com/404", {
+ retry: 3,
+ retryDelay: 500, // ms
+ retryStatusCodes: [404, 500], // response status codes to retry
+});
+```
+
+## ✔️ Timeout
+
+You can specify `timeout` in milliseconds to automatically abort a request after a timeout (default is disabled).
+
+```ts
+await ofetch("http://google.com/404", {
+ timeout: 3000, // Timeout after 3 seconds
+});
+```
+
+## ✔️ Type Friendly
+
+The response can be type assisted:
+
+```ts
+const article = await ofetch(`/api/article/${id}`);
+// Auto complete working with article.id
+```
+
+You can also type an instance created with `ofetch.create` using an
+OpenAPI-like `paths` schema (for example generated by `openapi-typescript`):
+
+```ts
+import type { paths } from "./autogenerated-types/my-api";
+
+const apiFetch = ofetch.create({ baseURL: "/api" });
+
+const user = await apiFetch("/users/{id}", {
+ method: "GET",
+ query: { expand: true },
+});
+```
+
+## ✔️ Adding `baseURL`
+
+By using `baseURL` option, `ofetch` prepends it for trailing/leading slashes and query search params for baseURL using [ufo](https://github.com/unjs/ufo):
+
+```js
+await ofetch("/config", { baseURL });
+```
+
+## ✔️ Adding Query Search Params
+
+By using `query` option (or `params` as alias), `ofetch` adds query search params to the URL by preserving the query in the request itself using [ufo](https://github.com/unjs/ufo):
+
+```js
+await ofetch("/movie?lang=en", { query: { id: 123 } });
+```
+
+## ✔️ Interceptors
+
+Providing async interceptors to hook into lifecycle events of `ofetch` call is possible.
+
+You might want to use `ofetch.create` to set shared interceptors.
+
+### `onRequest({ request, options })`
+
+`onRequest` is called as soon as `ofetch` is called, allowing you to modify options or do simple logging.
+
+```js
+await ofetch("/api", {
+ async onRequest({ request, options }) {
+ // Log request
+ console.log("[fetch request]", request, options);
+
+ // Add `?t=1640125211170` to query search params
+ options.query = options.query || {};
+ options.query.t = new Date();
+ },
+});
+```
+
+### `onRequestError({ request, options, error })`
+
+`onRequestError` will be called when the fetch request fails.
+
+```js
+await ofetch("/api", {
+ async onRequestError({ request, options, error }) {
+ // Log error
+ console.log("[fetch request error]", request, error);
+ },
+});
+```
+
+### `onResponse({ request, options, response })`
+
+`onResponse` will be called after `fetch` call and parsing body.
+
+```js
+await ofetch("/api", {
+ async onResponse({ request, response, options }) {
+ // Log response
+ console.log("[fetch response]", request, response.status, response.body);
+ },
+});
+```
+
+### `onResponseError({ request, options, response })`
+
+`onResponseError` is the same as `onResponse` but will be called when fetch happens but `response.ok` is not `true`.
+
+```js
+await ofetch("/api", {
+ async onResponseError({ request, response, options }) {
+ // Log error
+ console.log(
+ "[fetch response error]",
+ request,
+ response.status,
+ response.body
+ );
+ },
+});
+```
+
+### Passing array of interceptors
+
+If necessary, it's also possible to pass an array of function that will be called sequentially.
+
+```js
+await ofetch("/api", {
+ onRequest: [
+ () => {
+ /* Do something */
+ },
+ () => {
+ /* Do something else */
+ },
+ ],
+});
+```
+
+## ✔️ Create fetch with default options
+
+This utility is useful if you need to use common options across several fetch calls.
+
+**Note:** Defaults will be cloned at one level and inherited. Be careful about nested options like `headers`.
+
+```js
+const apiFetch = ofetch.create({ baseURL: "/api" });
+
+apiFetch("/test"); // Same as ofetch('/test', { baseURL: '/api' })
+```
+
+## 💡 Adding headers
+
+By using `headers` option, `ofetch` adds extra headers in addition to the request default headers:
+
+```js
+await ofetch("/movies", {
+ headers: {
+ Accept: "application/json",
+ "Cache-Control": "no-cache",
+ },
+});
+```
+
+## 🍣 Access to Raw Response
+
+If you need to access raw response (for headers, etc), you can use `ofetch.raw`:
+
+```js
+const response = await ofetch.raw("/sushi");
+
+// response._data
+// response.headers
+// ...
+```
+
+## 🌿 Using Native Fetch
+
+As a shortcut, you can use `ofetch.native` that provides native `fetch` API
+
+```js
+const json = await ofetch.native("/sushi").then((r) => r.json());
+```
+
+## 📡 SSE
+
+**Example:** Handle SSE response:
+
+```js
+const stream = await ofetch("/sse");
+const reader = stream.getReader();
+const decoder = new TextDecoder();
+while (true) {
+ const { done, value } = await reader.read();
+ if (done) break;
+ // Here is the chunked text of the SSE response.
+ const text = decoder.decode(value);
+}
+```
+
+## 🕵️ Proxy Support
+
+> [!IMPORTANT]
+> **Environment Variables:** Bun and Deno respect `HTTP_PROXY` and `HTTPS_PROXY` environment variables. Node.js requires setting `NODE_USE_ENV_PROXY=1` to enable [built-in proxy support](https://nodejs.org/api/http.html#http_built_in_proxy_support).
+
+### Node.js
+
+In Node.js (>= 18), you can use the `dispatcher` option with [undici](https://undici.nodejs.org/)'s `ProxyAgent`.
+
+```ts
+import { ProxyAgent } from "undici";
+
+const proxyAgent = new ProxyAgent("http://localhost:3128");
+
+await ofetch("https://icanhazip.com", { dispatcher: proxyAgent });
+```
+
+**Example:** Set proxy globally for all requests:
+
+```ts
+import { ProxyAgent, setGlobalDispatcher } from "undici";
+
+setGlobalDispatcher(new ProxyAgent("http://localhost:3128"));
+```
+
+**Example:** Allow self-signed certificates (USE AT YOUR OWN RISK!)
+
+```ts
+import { Agent } from "undici";
+
+// Note: This makes fetch unsecure against MITM attacks. USE AT YOUR OWN RISK!
+const unsecureAgent = new Agent({ connect: { rejectUnauthorized: false } });
+await ofetch("https://self-signed.example.com/", { dispatcher: unsecureAgent });
+```
+
+### Bun and Deno
+
+**Bun** supports the `proxy` option:
+
+```ts
+await ofetch("https://icanhazip.com", {
+ proxy: "http://localhost:3128",
+});
+```
+
+**Deno** can also use undici with npm specifiers for programmatic configuration.
+
+### 💪 Augment `FetchOptions` interface
+
+You can augment the `FetchOptions` interface to add custom properties.
+
+```ts
+// Place this in any `.ts` or `.d.ts` file.
+// Ensure it's included in the project's tsconfig.json "files".
+declare module "ofetch" {
+ interface FetchOptions {
+ // Custom properties
+ requiresAuth?: boolean;
+ }
+}
+
+export {};
+```
+
+This lets you pass and use those properties with full type safety throughout `ofetch` calls.
+
+```ts
+const myFetch = ofetch.create({
+ onRequest(context) {
+ // ^? { ..., options: {..., requiresAuth?: boolean }}
+ console.log(context.options.requiresAuth);
+ },
+});
+
+myFetch("/foo", { requiresAuth: true });
+```
+
+## License
+
+💛 Published under the [MIT](https://github.com/h3js/rou3/blob/main/LICENSE) license.
diff --git a/src/fetch.ts b/src/fetch.ts
index 10c91583..d22f1748 100644
--- a/src/fetch.ts
+++ b/src/fetch.ts
@@ -33,7 +33,9 @@ const retryStatusCodes = new Set([
// https://developer.mozilla.org/en-US/docs/Web/API/Response/body
const nullBodyResponses = new Set([101, 204, 205, 304]);
-export function createFetch(globalOptions: CreateFetchOptions = {}): $Fetch {
+export function createFetch(
+ globalOptions: CreateFetchOptions = {}
+): $Fetch {
const { fetch = globalThis.fetch } = globalOptions;
async function onError(context: FetchContext): Promise> {
@@ -256,10 +258,13 @@ export function createFetch(globalOptions: CreateFetchOptions = {}): $Fetch {
return context.response;
};
- const $fetch = async function $fetch(request, options) {
+ const $fetch = async function $fetch(
+ request: FetchRequest,
+ options?: FetchOptions
+ ) {
const r = await $fetchRaw(request, options);
return r._data;
- } as $Fetch;
+ } as $Fetch;
$fetch.raw = $fetchRaw;
diff --git a/src/types.ts b/src/types.ts
index 66a84faf..29be654c 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -2,9 +2,170 @@
// $fetch API
// --------------------------
-export interface $Fetch {
- (
- request: FetchRequest,
+type HttpMethod =
+ | "get"
+ | "post"
+ | "put"
+ | "patch"
+ | "delete"
+ | "head"
+ | "options"
+ | "trace";
+
+type _UntypedSchemaMarker = void;
+
+type PathsForSchema = Extract;
+
+type PathsWithMethod = {
+ [TPath in PathsForSchema]: TMethod extends MethodsForPath<
+ TSchema,
+ TPath
+ >
+ ? TPath
+ : never;
+}[PathsForSchema];
+
+type GetMethodForPath> = Extract<
+ "get",
+ MethodsForPath
+>;
+
+type MethodInputForPath> =
+ | MethodsForPath
+ | Uppercase>;
+
+type MethodFromInput<
+ TSchema,
+ TPath extends PathsForSchema,
+ TInput extends MethodInputForPath,
+> = Extract, MethodsForPath>;
+
+type MethodsForPath> = Extract<
+ Extract,
+ HttpMethod
+>;
+
+type OperationFor<
+ TSchema,
+ TPath extends PathsForSchema,
+ TMethod extends MethodsForPath,
+> = TSchema[TPath][TMethod];
+
+type UntypedRequest = [
+ TSchema,
+] extends [_UntypedSchemaMarker]
+ ? TRequest
+ : TRequest extends string
+ ? never
+ : TRequest;
+
+type OperationRequestBody = T extends { requestBody: infer TRequestBody }
+ ? TRequestBody extends { content: infer TContent }
+ ? TContent[keyof TContent]
+ : never
+ : never;
+
+type OperationHasRequiredBody = T extends {
+ requestBody: { required: true };
+}
+ ? true
+ : false;
+
+type OperationResponses = T extends { responses: infer TResponses }
+ ? TResponses
+ : never;
+
+type SuccessStatusCode = `${2}${number}${number}` | "2XX" | "default";
+
+type SuccessResponses =
+ TResponses extends Record
+ ? {
+ [TStatus in keyof TResponses]: `${TStatus & (string | number)}` extends SuccessStatusCode
+ ? TResponses[TStatus]
+ : never;
+ }[keyof TResponses]
+ : never;
+
+type ResponseBodyFromResponse = T extends { content: infer TContent }
+ ? TContent[keyof TContent]
+ : never;
+
+type OperationData = ResponseBodyFromResponse<
+ SuccessResponses>
+>;
+
+type OperationQuery = T extends { parameters: infer TParameters }
+ ? TParameters extends { query: infer TQuery }
+ ? TQuery
+ : never
+ : never;
+
+type QueryOption = [OperationQuery] extends [never]
+ ? {}
+ : {
+ query?: OperationQuery;
+ /**
+ * @deprecated use query instead.
+ */
+ params?: OperationQuery;
+ };
+
+type BodyOption = [OperationRequestBody] extends [never]
+ ? {}
+ : OperationHasRequiredBody extends true
+ ? { body: OperationRequestBody }
+ : { body?: OperationRequestBody };
+
+type TypedFetchOptions = Omit<
+ FetchOptions>,
+ "query" | "params" | "body"
+> &
+ QueryOption &
+ BodyOption;
+
+export interface $Fetch {
+ <
+ TPath extends PathsForSchema,
+ TMethodInput extends MethodInputForPath,
+ TMethod extends MethodsForPath = MethodFromInput<
+ TSchema,
+ TPath,
+ TMethodInput
+ >,
+ R extends ResponseType = "json",
+ >(
+ request: TPath,
+ options: TypedFetchOptions, R> & {
+ method: TMethodInput;
+ }
+ ): Promise<
+ MappedResponseType>>
+ >;
+ <
+ TPath extends PathsWithMethod,
+ R extends ResponseType = "json",
+ >(
+ request: TPath,
+ options?: TypedFetchOptions<
+ OperationFor>,
+ R
+ > & {
+ method?: "get" | "GET";
+ }
+ ): Promise<
+ MappedResponseType<
+ R,
+ OperationData<
+ OperationFor>
+ >
+ >
+ >;
+ <
+ T = any,
+ R extends ResponseType = "json",
+ TRequest extends FetchRequest = FetchRequest,
+ >(
+ request: UntypedRequest,
options?: FetchOptions
): Promise>;
raw(
@@ -12,7 +173,10 @@ export interface $Fetch {
options?: FetchOptions
): Promise>>;
native: Fetch;
- create(defaults: FetchOptions, globalOptions?: CreateFetchOptions): $Fetch;
+ create(
+ defaults: FetchOptions,
+ globalOptions?: CreateFetchOptions
+ ): $Fetch;
}
// --------------------------
diff --git a/test/types.test.ts b/test/types.test.ts
new file mode 100644
index 00000000..6bcdcfa6
--- /dev/null
+++ b/test/types.test.ts
@@ -0,0 +1,109 @@
+import { describe, it } from "vitest";
+import { createFetch } from "../src/index.ts";
+
+type ApiPaths = {
+ "/users": {
+ get: {
+ responses: {
+ 200: { content: { "application/json": { users: string[] } } };
+ default: { content: { "application/json": { message: string } } };
+ };
+ };
+ post: {
+ requestBody: {
+ required: true;
+ content: { "application/json": { name: string } };
+ };
+ responses: {
+ 201: { content: { "application/json": { id: string } } };
+ };
+ };
+ };
+ "/users/{id}": {
+ get: {
+ parameters: {
+ path: { id: string };
+ query: { expand?: boolean };
+ };
+ responses: {
+ 200: { content: { "application/json": { id: string; name: string } } };
+ 404: { content: { "application/json": { message: string } } };
+ };
+ };
+ delete: {
+ parameters: {
+ path: { id: string };
+ };
+ responses: {
+ 204: Record;
+ };
+ };
+ };
+ "/health": {
+ get: {
+ responses: {
+ 200: { content: { "text/plain": "ok" } };
+ };
+ };
+ };
+};
+
+describe("typed create", () => {
+ const typedFetch = createFetch({
+ fetch: () =>
+ Promise.resolve(
+ new Response('{"users":[]}', {
+ status: 200,
+ headers: { "content-type": "application/json" },
+ })
+ ),
+ });
+
+ it("infers path, method, body and response types", () => {
+ // GET /users — default response should not include `default` error branch
+ const _users: Promise<{ users: string[] }> = typedFetch("/users");
+
+ // POST /users — required body enforced, response from 201
+ const _created: Promise<{ id: string }> = typedFetch("/users", {
+ method: "POST",
+ body: { name: "Ada" },
+ });
+
+ // GET with responseType override
+ const _health: Promise = typedFetch("/health", {
+ responseType: "text",
+ });
+
+ // GET /users/{id} — path params and optional query params
+ const _user: Promise<{ id: string; name: string }> = typedFetch(
+ "/users/{id}",
+ { pathParams: { id: "1" }, query: { expand: true } }
+ );
+
+ // DELETE /users/{id} — 204 no-content resolves to undefined
+ const _deleted: Promise = typedFetch("/users/{id}", {
+ method: "DELETE",
+ pathParams: { id: "1" },
+ });
+
+ // @ts-expect-error Missing required request body for POST operation.
+ typedFetch("/users", { method: "POST" });
+
+ // @ts-expect-error PATCH is not an available method on /users.
+ typedFetch("/users", { method: "PATCH" });
+
+ // @ts-expect-error Path is not part of the schema.
+ typedFetch("/unknown");
+ });
+
+ it("keeps legacy generic usage", () => {
+ const _legacy: Promise = typedFetch(
+ new Request("https://example.com")
+ );
+ });
+
+ it("supports ofetch.create with schema type", () => {
+ const apiFetch = typedFetch.create({ baseURL: "/api" });
+ const _users: Promise<{ users: string[] }> = apiFetch("/users");
+ });
+});