Skip to content

feat(types): add schema-typed ofetch.create with OpenAPI-style path inference (#406)#548

Open
gkoos wants to merge 2 commits intounjs:mainfrom
gkoos:feat/typed-create-openapi-paths
Open

feat(types): add schema-typed ofetch.create with OpenAPI-style path inference (#406)#548
gkoos wants to merge 2 commits intounjs:mainfrom
gkoos:feat/typed-create-openapi-paths

Conversation

@gkoos
Copy link
Copy Markdown

@gkoos gkoos commented Mar 26, 2026

Fixes: #406

Adds type support for passing OpenAPI-style schema types to ofetch.create (for example from openapi-typescript), enabling typed path/method/body/response inference.

Changes:

  • make createFetch generic and return schema-aware $Fetch
  • add typed $Fetch overloads for schema-based path/method inference
  • keep legacy non-schema usage intact
  • add type tests for valid and invalid typed calls
  • add a short README example for ofetch.create()

Summary by CodeRabbit

  • New Features

    • Schema-driven, path-and-method-aware typing for fetch calls and for creating typed fetch instances.
  • Documentation

    • README rewritten with examples demonstrating the new "Type Friendly" schema and create-typing usage.
  • Tests

    • Added type tests validating schema inference, correct request/response typings, and expected type errors for invalid calls.

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 26, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: e53a4821-35d6-4c25-96a6-0ccdbea0e3ee

📥 Commits

Reviewing files that changed from the base of the PR and between e55f931 and 994f599.

📒 Files selected for processing (1)
  • test/types.test.ts
✅ Files skipped from review due to trivial changes (1)
  • test/types.test.ts

📝 Walkthrough

Walkthrough

Adds schema-driven TypeScript typing to createFetch, refactoring $Fetch into path- and method-aware overloads and updating createFetch/create to be generic so API paths (OpenAPI-like paths) drive request/response typings. Includes new type tests and README updates.

Changes

Cohort / File(s) Summary
Core fetch implementation
src/fetch.ts
createFetch is now generic <TSchema = void> and returns $Fetch<TSchema>; internal $fetch wrapper typed to align with the schema generic.
Type system & public types
src/types.ts
Refactor of export interface $Fetch<TSchema = _UntypedSchemaMarker> into schema-typed overloads (path/method-aware), added internal type utilities to extract paths, methods, request bodies, query/params, and responses; create method made generic to return $Fetch<TNewSchema>.
Type tests
test/types.test.ts
New Vitest TypeScript tests exercising createFetch<ApiPaths>: verifies inferred return types, required/optional bodies and params, responseType variants, 204->undefined mapping, and // @ts-expect-error`` cases for invalid calls.
Documentation
README.md
Large rewrite/addition documenting the new “Type Friendly” section and examples showing ofetch.create typing via an OpenAPI-like paths schema; examples updated to demonstrate typed usage.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

🐇 I nibble on schemas, lines crisp and neat,

Paths hop to methods, types tidy and sweet,
Autocomplete flutters, no more blind guess,
Ofetch now sings with typed happiness!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main change: adding schema-typed support to ofetch.create with OpenAPI-style path inference, matching the PR's primary objective.
Linked Issues check ✅ Passed All coding requirements from issue #406 are met: createFetch is generic over TSchema, $Fetch includes typed overloads for path/method/body/response inference, legacy usage is preserved, and type tests validate both valid and invalid calls.
Out of Scope Changes check ✅ Passed All changes are directly scoped to issue #406: type definitions, generic factory function, type tests, and README documentation of the new feature. No unrelated modifications detected.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (1)
test/types.test.ts (1)

4-81: Use a fixture that matches the public OpenAPI contract.

This only exercises createFetch<ApiPaths>() with static paths and method-local shapes, so it won’t catch the cases this PR is advertising: ofetch.create<paths>(), templated routes, shared paths["..."]["parameters"], default error responses, or 204/no-content responses. Add at least one realistic "/users/{id}" case through .create<paths>() so the intended integration is actually pinned down. (openapi-ts.dev)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/types.test.ts` around lines 4 - 81, The test fixture ApiPaths used by
createFetch in the typed tests is too minimal and misses templated routes and
shared parameter/response cases; update the ApiPaths type and the test calls to
include a realistic templated route such as "/users/{id}" with path parameters
and multiple response shapes (e.g., 200 JSON user, 204 no-content, default error
response), exercise createFetch via createFetch<ApiPaths> (the typedFetch
constant) and add calls that use templated route invocation, path parameter
typing, and default/error responses so the test covers ofetch.create<paths>(),
templated routes, shared paths["..."]["parameters"], and 204/no-content behavior
referenced in the review comment.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/types.ts`:
- Around line 176-179: The declaration for create<TNewSchema = TSchema>
currently requires defaults, causing valid runtime calls like ofetch.create() to
type-error; update the signature in src/types.ts so defaults is optional (or has
a default empty object) e.g. make the first parameter optional (defaults?:
FetchOptions) or defaults: FetchOptions | undefined to match src/fetch.ts which
implements create(defaultOptions = {}, customGlobalOptions = {}), keeping
globalOptions (CreateFetchOptions) optional and preserving the generic
$Fetch<TNewSchema> return type.
- Around line 89-95: ResponseBodyFromResponse currently returns never when a
success response has no content, causing endpoints like 204 to type as
Promise<never>; update ResponseBodyFromResponse so that if T has no content or
its content type has no keys (keyof TContent is never) it resolves to undefined,
otherwise return TContent[keyof TContent]; keep OperationData<T> as
ResponseBodyFromResponse<SuccessResponses<OperationResponses<T>>> so callers get
undefined for contentless 2xx responses instead of never.
- Around line 78-87: SuccessStatusCode wrongly includes "default" causing
SuccessResponses to include the default error branch; update the types so
"default" is excluded from successful status resolution. Concretely, modify
SuccessStatusCode (or the mapping in SuccessResponses) to only recognize 2xx
codes and "2XX" (remove "default") or add an explicit conditional in
SuccessResponses to treat TStatus extends "default" ? never : ... so that keys
named "default" are never included in the resolved success payload; ensure the
symbols to change are SuccessStatusCode and/or the SuccessResponses mapped type.
- Around line 97-124: OperationQuery only extracts query params and
TypedFetchOptions lacks any slot for path parameters, so add a path-parameters
extraction and include it in the options: introduce a new type (e.g.,
OperationPath<T>) that mirrors OperationQuery but extracts parameters with "in:
'path'" (or rework OperationQuery to return a union of query and path objects),
create a PathOption<T> (similar to QueryOption<T>) that exposes a typed
pathParams?: OperationPath<T> (or required body-style when path params are
required), and extend TypedFetchOptions<T, R> to intersect PathOption<T> so
callers can supply path parameter values; also update any request invocation
type (the function that accepts the literal path) to accept a templated path +
pathParams or a function overload that accepts a path template string and a
typed params object, referencing OperationQuery, QueryOption, BodyOption,
TypedFetchOptions, OperationRequestBody, OperationHasRequiredBody, FetchOptions,
and OperationData to locate the relevant types to modify.

---

Nitpick comments:
In `@test/types.test.ts`:
- Around line 4-81: The test fixture ApiPaths used by createFetch in the typed
tests is too minimal and misses templated routes and shared parameter/response
cases; update the ApiPaths type and the test calls to include a realistic
templated route such as "/users/{id}" with path parameters and multiple response
shapes (e.g., 200 JSON user, 204 no-content, default error response), exercise
createFetch via createFetch<ApiPaths> (the typedFetch constant) and add calls
that use templated route invocation, path parameter typing, and default/error
responses so the test covers ofetch.create<paths>(), templated routes, shared
paths["..."]["parameters"], and 204/no-content behavior referenced in the review
comment.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: e94f54a9-2be0-47dc-93e0-45917e49c676

📥 Commits

Reviewing files that changed from the base of the PR and between dfbe3ca and e55f931.

📒 Files selected for processing (4)
  • README.md
  • src/fetch.ts
  • src/types.ts
  • test/types.test.ts

Comment on lines +78 to +87
type SuccessStatusCode = `${2}${number}${number}` | "2XX" | "default";

type SuccessResponses<TResponses> =
TResponses extends Record<string, any>
? {
[TStatus in keyof TResponses]: `${TStatus & (string | number)}` extends SuccessStatusCode
? TResponses[TStatus]
: never;
}[keyof TResponses]
: never;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Don’t fold default into the resolved success payload.

SuccessStatusCode currently treats default like a 2xx branch, so a schema like { 200: User; default: ApiError } resolves as Promise<User | ApiError>. Since src/fetch.ts throws on 4xx/5xx unless ignoreResponseError is set, that makes the normal success type too wide.

Suggested diff
-type SuccessStatusCode = `${2}${number}${number}` | "2XX" | "default";
+type SuccessStatusCode = `${2}${number}${number}` | "2XX";
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/types.ts` around lines 78 - 87, SuccessStatusCode wrongly includes
"default" causing SuccessResponses to include the default error branch; update
the types so "default" is excluded from successful status resolution.
Concretely, modify SuccessStatusCode (or the mapping in SuccessResponses) to
only recognize 2xx codes and "2XX" (remove "default") or add an explicit
conditional in SuccessResponses to treat TStatus extends "default" ? never : ...
so that keys named "default" are never included in the resolved success payload;
ensure the symbols to change are SuccessStatusCode and/or the SuccessResponses
mapped type.

Comment on lines +89 to +95
type ResponseBodyFromResponse<T> = T extends { content: infer TContent }
? TContent[keyof TContent]
: never;

type OperationData<T> = ResponseBodyFromResponse<
SuccessResponses<OperationResponses<T>>
>;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Return undefined for contentless 2xx responses.

If a success response omits content, ResponseBodyFromResponse becomes never, so common 204 endpoints type as Promise<never> even though runtime resolves with _data === undefined.

Suggested diff
 type ResponseBodyFromResponse<T> = T extends { content: infer TContent }
   ? TContent[keyof TContent]
-  : never;
+  : undefined;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
type ResponseBodyFromResponse<T> = T extends { content: infer TContent }
? TContent[keyof TContent]
: never;
type OperationData<T> = ResponseBodyFromResponse<
SuccessResponses<OperationResponses<T>>
>;
type ResponseBodyFromResponse<T> = T extends { content: infer TContent }
? TContent[keyof TContent]
: undefined;
type OperationData<T> = ResponseBodyFromResponse<
SuccessResponses<OperationResponses<T>>
>;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/types.ts` around lines 89 - 95, ResponseBodyFromResponse currently
returns never when a success response has no content, causing endpoints like 204
to type as Promise<never>; update ResponseBodyFromResponse so that if T has no
content or its content type has no keys (keyof TContent is never) it resolves to
undefined, otherwise return TContent[keyof TContent]; keep OperationData<T> as
ResponseBodyFromResponse<SuccessResponses<OperationResponses<T>>> so callers get
undefined for contentless 2xx responses instead of never.

Comment on lines +176 to +179
create<TNewSchema = TSchema>(
defaults: FetchOptions,
globalOptions?: CreateFetchOptions
): $Fetch<TNewSchema>;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Keep .create() callable without arguments.

Runtime still supports ofetch.create() because src/fetch.ts implements create(defaultOptions = {}, customGlobalOptions = {}). Making defaults mandatory here turns an existing valid call into a type error with no behavior change.

Suggested diff
   create<TNewSchema = TSchema>(
-    defaults: FetchOptions,
+    defaults?: FetchOptions,
     globalOptions?: CreateFetchOptions
   ): $Fetch<TNewSchema>;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/types.ts` around lines 176 - 179, The declaration for create<TNewSchema =
TSchema> currently requires defaults, causing valid runtime calls like
ofetch.create() to type-error; update the signature in src/types.ts so defaults
is optional (or has a default empty object) e.g. make the first parameter
optional (defaults?: FetchOptions) or defaults: FetchOptions | undefined to
match src/fetch.ts which implements create(defaultOptions = {},
customGlobalOptions = {}), keeping globalOptions (CreateFetchOptions) optional
and preserving the generic $Fetch<TNewSchema> return type.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add support of TS type annotations on ofetch.create

1 participant