Skip to content
16 changes: 16 additions & 0 deletions .changeset/unwrap-return-type.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
---
'@kubb/plugin-axios': minor
'@kubb/plugin-fetch': minor
'@kubb/plugin-react-query': minor
'@kubb/plugin-vue-query': minor
'@kubb/plugin-swr': minor
---

Add a `returnType` option (`'full' | 'data'`, default `'full'`) to the standalone client
functions and the class-based SDK. `'data'` resolves a call to the bare success body instead of
the full `{ status, data, error, contentType, request, response }` result, once `throwOnError`
(on by default) rules out the error branch.

`plugin-react-query`, `plugin-vue-query`, and `plugin-swr` now read this option off the
registered client plugin, so their generated hooks work with either setting instead of assuming
the full result.
24 changes: 24 additions & 0 deletions examples/advanced/src/gen/.kubb/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,30 @@ export type RequestResult<TResponses, ThrowOnError extends boolean = true, TRequ
? { status: number; data: undefined; error: undefined; contentType: string | undefined; request: TRequest; response: TResponse }
: ResultUnion<TResponses, TRequest, TResponse>

/**
* The shape a generated operation returns when `returnType: 'data'` is set: the bare success body
* once `throwOnError` (on by default) narrows away the error branch, falling back to the full
* `RequestResult` when a call sets `throwOnError: false` and still needs `error` to discriminate a
* failed response.
*/
export type UnwrappedResult<
TResponses,
ThrowOnError extends boolean = true,
TRequest = AxiosRequestConfig,
TResponse = AxiosResponse,
> = ThrowOnError extends true ? RequestResult<TResponses, true, TRequest, TResponse>['data'] : RequestResult<TResponses, ThrowOnError, TRequest, TResponse>

/**
* Narrows a resolved call down to its success body once `throwOnError` (on by default) rules out
* the error branch, the same default the runtime itself applies. Falls back to the full result for
* a call that sets `throwOnError: false`, since that path still needs `error` to discriminate a
* failed response. Backs `returnType: 'data'`, mirroring how `toEventStream` centralizes the
* post-processing for `text/event-stream` operations.
*/
export function unwrapResult<T extends { data: unknown; error: unknown }>(promise: Promise<T>, throwOnError: boolean | undefined): Promise<T | T['data']> {
return promise.then((result) => ((throwOnError ?? true) ? result.data : result))
}

/**
* The data-shaped keys of the grouped options object, which `Options` re-adds typed per operation.
*/
Expand Down
24 changes: 24 additions & 0 deletions examples/axios/src/gen/.kubb/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,30 @@ export type RequestResult<TResponses, ThrowOnError extends boolean = true, TRequ
? { status: number; data: undefined; error: undefined; contentType: string | undefined; request: TRequest; response: TResponse }
: ResultUnion<TResponses, TRequest, TResponse>

/**
* The shape a generated operation returns when `returnType: 'data'` is set: the bare success body
* once `throwOnError` (on by default) narrows away the error branch, falling back to the full
* `RequestResult` when a call sets `throwOnError: false` and still needs `error` to discriminate a
* failed response.
*/
export type UnwrappedResult<
TResponses,
ThrowOnError extends boolean = true,
TRequest = AxiosRequestConfig,
TResponse = AxiosResponse,
> = ThrowOnError extends true ? RequestResult<TResponses, true, TRequest, TResponse>['data'] : RequestResult<TResponses, ThrowOnError, TRequest, TResponse>

/**
* Narrows a resolved call down to its success body once `throwOnError` (on by default) rules out
* the error branch, the same default the runtime itself applies. Falls back to the full result for
* a call that sets `throwOnError: false`, since that path still needs `error` to discriminate a
* failed response. Backs `returnType: 'data'`, mirroring how `toEventStream` centralizes the
* post-processing for `text/event-stream` operations.
*/
export function unwrapResult<T extends { data: unknown; error: unknown }>(promise: Promise<T>, throwOnError: boolean | undefined): Promise<T | T['data']> {
return promise.then((result) => ((throwOnError ?? true) ? result.data : result))
}

/**
* The data-shaped keys of the grouped options object, which `Options` re-adds typed per operation.
*/
Expand Down
21 changes: 21 additions & 0 deletions examples/fetch/src/gen/.kubb/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,27 @@ export type RequestResult<TResponses, ThrowOnError extends boolean = true, TRequ
? { status: number; data: undefined; error: undefined; contentType: string | undefined; request: TRequest; response: TResponse }
: ResultUnion<TResponses, TRequest, TResponse>

/**
* The shape a generated operation returns when `returnType: 'data'` is set: the bare success body
* once `throwOnError` (on by default) narrows away the error branch, falling back to the full
* `RequestResult` when a call sets `throwOnError: false` and still needs `error` to discriminate a
* failed response.
*/
export type UnwrappedResult<TResponses, ThrowOnError extends boolean = true, TRequest = Request, TResponse = Response> = ThrowOnError extends true
? RequestResult<TResponses, true, TRequest, TResponse>['data']
: RequestResult<TResponses, ThrowOnError, TRequest, TResponse>

/**
* Narrows a resolved call down to its success body once `throwOnError` (on by default) rules out
* the error branch, the same default the runtime itself applies. Falls back to the full result for
* a call that sets `throwOnError: false`, since that path still needs `error` to discriminate a
* failed response. Backs `returnType: 'data'`, mirroring how `toEventStream` centralizes the
* post-processing for `text/event-stream` operations.
*/
export function unwrapResult<T extends { data: unknown; error: unknown }>(promise: Promise<T>, throwOnError: boolean | undefined): Promise<T | T['data']> {
return promise.then((result) => ((throwOnError ?? true) ? result.data : result))
}

/**
* The data-shaped keys of the grouped options object, which `Options` re-adds typed per operation.
*/
Expand Down
24 changes: 24 additions & 0 deletions examples/mcp/src/gen/.kubb/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,30 @@ export type RequestResult<TResponses, ThrowOnError extends boolean = true, TRequ
? { status: number; data: undefined; error: undefined; contentType: string | undefined; request: TRequest; response: TResponse }
: ResultUnion<TResponses, TRequest, TResponse>

/**
* The shape a generated operation returns when `returnType: 'data'` is set: the bare success body
* once `throwOnError` (on by default) narrows away the error branch, falling back to the full
* `RequestResult` when a call sets `throwOnError: false` and still needs `error` to discriminate a
* failed response.
*/
export type UnwrappedResult<
TResponses,
ThrowOnError extends boolean = true,
TRequest = AxiosRequestConfig,
TResponse = AxiosResponse,
> = ThrowOnError extends true ? RequestResult<TResponses, true, TRequest, TResponse>['data'] : RequestResult<TResponses, ThrowOnError, TRequest, TResponse>

/**
* Narrows a resolved call down to its success body once `throwOnError` (on by default) rules out
* the error branch, the same default the runtime itself applies. Falls back to the full result for
* a call that sets `throwOnError: false`, since that path still needs `error` to discriminate a
* failed response. Backs `returnType: 'data'`, mirroring how `toEventStream` centralizes the
* post-processing for `text/event-stream` operations.
*/
export function unwrapResult<T extends { data: unknown; error: unknown }>(promise: Promise<T>, throwOnError: boolean | undefined): Promise<T | T['data']> {
return promise.then((result) => ((throwOnError ?? true) ? result.data : result))
}

/**
* The data-shaped keys of the grouped options object, which `Options` re-adds typed per operation.
*/
Expand Down
21 changes: 21 additions & 0 deletions examples/react-query/src/gen/.kubb/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,27 @@ export type RequestResult<TResponses, ThrowOnError extends boolean = true, TRequ
? { status: number; data: undefined; error: undefined; contentType: string | undefined; request: TRequest; response: TResponse }
: ResultUnion<TResponses, TRequest, TResponse>

/**
* The shape a generated operation returns when `returnType: 'data'` is set: the bare success body
* once `throwOnError` (on by default) narrows away the error branch, falling back to the full
* `RequestResult` when a call sets `throwOnError: false` and still needs `error` to discriminate a
* failed response.
*/
export type UnwrappedResult<TResponses, ThrowOnError extends boolean = true, TRequest = Request, TResponse = Response> = ThrowOnError extends true
? RequestResult<TResponses, true, TRequest, TResponse>['data']
: RequestResult<TResponses, ThrowOnError, TRequest, TResponse>

/**
* Narrows a resolved call down to its success body once `throwOnError` (on by default) rules out
* the error branch, the same default the runtime itself applies. Falls back to the full result for
* a call that sets `throwOnError: false`, since that path still needs `error` to discriminate a
* failed response. Backs `returnType: 'data'`, mirroring how `toEventStream` centralizes the
* post-processing for `text/event-stream` operations.
*/
export function unwrapResult<T extends { data: unknown; error: unknown }>(promise: Promise<T>, throwOnError: boolean | undefined): Promise<T | T['data']> {
return promise.then((result) => ((throwOnError ?? true) ? result.data : result))
}

/**
* The data-shaped keys of the grouped options object, which `Options` re-adds typed per operation.
*/
Expand Down
21 changes: 21 additions & 0 deletions examples/sdk/src/gen/.kubb/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,27 @@ export type RequestResult<TResponses, ThrowOnError extends boolean = true, TRequ
? { status: number; data: undefined; error: undefined; contentType: string | undefined; request: TRequest; response: TResponse }
: ResultUnion<TResponses, TRequest, TResponse>

/**
* The shape a generated operation returns when `returnType: 'data'` is set: the bare success body
* once `throwOnError` (on by default) narrows away the error branch, falling back to the full
* `RequestResult` when a call sets `throwOnError: false` and still needs `error` to discriminate a
* failed response.
*/
export type UnwrappedResult<TResponses, ThrowOnError extends boolean = true, TRequest = Request, TResponse = Response> = ThrowOnError extends true
? RequestResult<TResponses, true, TRequest, TResponse>['data']
: RequestResult<TResponses, ThrowOnError, TRequest, TResponse>

/**
* Narrows a resolved call down to its success body once `throwOnError` (on by default) rules out
* the error branch, the same default the runtime itself applies. Falls back to the full result for
* a call that sets `throwOnError: false`, since that path still needs `error` to discriminate a
* failed response. Backs `returnType: 'data'`, mirroring how `toEventStream` centralizes the
* post-processing for `text/event-stream` operations.
*/
export function unwrapResult<T extends { data: unknown; error: unknown }>(promise: Promise<T>, throwOnError: boolean | undefined): Promise<T | T['data']> {
return promise.then((result) => ((throwOnError ?? true) ? result.data : result))
}

/**
* The data-shaped keys of the grouped options object, which `Options` re-adds typed per operation.
*/
Expand Down
21 changes: 21 additions & 0 deletions examples/simple-single/src/gen/.kubb/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,27 @@ export type RequestResult<TResponses, ThrowOnError extends boolean = true, TRequ
? { status: number; data: undefined; error: undefined; contentType: string | undefined; request: TRequest; response: TResponse }
: ResultUnion<TResponses, TRequest, TResponse>

/**
* The shape a generated operation returns when `returnType: 'data'` is set: the bare success body
* once `throwOnError` (on by default) narrows away the error branch, falling back to the full
* `RequestResult` when a call sets `throwOnError: false` and still needs `error` to discriminate a
* failed response.
*/
export type UnwrappedResult<TResponses, ThrowOnError extends boolean = true, TRequest = Request, TResponse = Response> = ThrowOnError extends true
? RequestResult<TResponses, true, TRequest, TResponse>['data']
: RequestResult<TResponses, ThrowOnError, TRequest, TResponse>

/**
* Narrows a resolved call down to its success body once `throwOnError` (on by default) rules out
* the error branch, the same default the runtime itself applies. Falls back to the full result for
* a call that sets `throwOnError: false`, since that path still needs `error` to discriminate a
* failed response. Backs `returnType: 'data'`, mirroring how `toEventStream` centralizes the
* post-processing for `text/event-stream` operations.
*/
export function unwrapResult<T extends { data: unknown; error: unknown }>(promise: Promise<T>, throwOnError: boolean | undefined): Promise<T | T['data']> {
return promise.then((result) => ((throwOnError ?? true) ? result.data : result))
}

/**
* The data-shaped keys of the grouped options object, which `Options` re-adds typed per operation.
*/
Expand Down
21 changes: 21 additions & 0 deletions examples/swr/src/gen/.kubb/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,27 @@ export type RequestResult<TResponses, ThrowOnError extends boolean = true, TRequ
? { status: number; data: undefined; error: undefined; contentType: string | undefined; request: TRequest; response: TResponse }
: ResultUnion<TResponses, TRequest, TResponse>

/**
* The shape a generated operation returns when `returnType: 'data'` is set: the bare success body
* once `throwOnError` (on by default) narrows away the error branch, falling back to the full
* `RequestResult` when a call sets `throwOnError: false` and still needs `error` to discriminate a
* failed response.
*/
export type UnwrappedResult<TResponses, ThrowOnError extends boolean = true, TRequest = Request, TResponse = Response> = ThrowOnError extends true
? RequestResult<TResponses, true, TRequest, TResponse>['data']
: RequestResult<TResponses, ThrowOnError, TRequest, TResponse>

/**
* Narrows a resolved call down to its success body once `throwOnError` (on by default) rules out
* the error branch, the same default the runtime itself applies. Falls back to the full result for
* a call that sets `throwOnError: false`, since that path still needs `error` to discriminate a
* failed response. Backs `returnType: 'data'`, mirroring how `toEventStream` centralizes the
* post-processing for `text/event-stream` operations.
*/
export function unwrapResult<T extends { data: unknown; error: unknown }>(promise: Promise<T>, throwOnError: boolean | undefined): Promise<T | T['data']> {
return promise.then((result) => ((throwOnError ?? true) ? result.data : result))
}

/**
* The data-shaped keys of the grouped options object, which `Options` re-adds typed per operation.
*/
Expand Down
21 changes: 21 additions & 0 deletions examples/vue-query/src/gen/.kubb/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,27 @@ export type RequestResult<TResponses, ThrowOnError extends boolean = true, TRequ
? { status: number; data: undefined; error: undefined; contentType: string | undefined; request: TRequest; response: TResponse }
: ResultUnion<TResponses, TRequest, TResponse>

/**
* The shape a generated operation returns when `returnType: 'data'` is set: the bare success body
* once `throwOnError` (on by default) narrows away the error branch, falling back to the full
* `RequestResult` when a call sets `throwOnError: false` and still needs `error` to discriminate a
* failed response.
*/
export type UnwrappedResult<TResponses, ThrowOnError extends boolean = true, TRequest = Request, TResponse = Response> = ThrowOnError extends true
? RequestResult<TResponses, true, TRequest, TResponse>['data']
: RequestResult<TResponses, ThrowOnError, TRequest, TResponse>

/**
* Narrows a resolved call down to its success body once `throwOnError` (on by default) rules out
* the error branch, the same default the runtime itself applies. Falls back to the full result for
* a call that sets `throwOnError: false`, since that path still needs `error` to discriminate a
* failed response. Backs `returnType: 'data'`, mirroring how `toEventStream` centralizes the
* post-processing for `text/event-stream` operations.
*/
export function unwrapResult<T extends { data: unknown; error: unknown }>(promise: Promise<T>, throwOnError: boolean | undefined): Promise<T | T['data']> {
return promise.then((result) => ((throwOnError ?? true) ? result.data : result))
}

/**
* The data-shaped keys of the grouped options object, which `Options` re-adds typed per operation.
*/
Expand Down
12 changes: 11 additions & 1 deletion internals/client/src/builders/generics.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { ast } from 'kubb/kit'
import { resolverTs } from '@kubb/plugin-ts'
import { describe, expect, test } from 'vitest'
import { buildRequestResultGenerics } from './generics.ts'
import { buildRequestResultGenerics, buildResultType } from './generics.ts'

const node = ast.factory.createOperation({
operationId: 'getPetById',
Expand All @@ -17,3 +17,13 @@ describe('buildRequestResultGenerics', () => {
expect(buildRequestResultGenerics({ node, types: resolverTs })).toBe('GetPetByIdResponses, ThrowOnError')
})
})

describe('buildResultType', () => {
test('names RequestResult for the default full return type', () => {
expect(buildResultType({ node, types: resolverTs, returnType: 'full' })).toBe('RequestResult<GetPetByIdResponses, ThrowOnError>')
})

test('names UnwrappedResult when returnType is data', () => {
expect(buildResultType({ node, types: resolverTs, returnType: 'data' })).toBe('UnwrappedResult<GetPetByIdResponses, ThrowOnError>')
})
})
14 changes: 14 additions & 0 deletions internals/client/src/builders/generics.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { ast } from 'kubb/kit'
import type { OperationTypeNames } from '../resolveOperationTypes.ts'
import type { ReturnTypeOption } from '../types.ts'

/**
* Builds the `RequestResult` generic arguments for one operation: the per-status responses record
Expand All @@ -12,3 +13,16 @@ import type { OperationTypeNames } from '../resolveOperationTypes.ts'
export function buildRequestResultGenerics({ node, types }: { node: ast.OperationNode; types: OperationTypeNames }): string {
return `${types.response.responses(node)}, ThrowOnError`
}

/**
* Builds the result type name an operation's function signature and return statement use:
* `RequestResult` for the default `returnType: 'full'`, or the runtime's `UnwrappedResult` when
* `returnType: 'data'` narrows a resolved call down to the bare success body.
*
* @example
* `buildResultType({ node, types, returnType: 'data' }) // 'UnwrappedResult<AddPetResponses, ThrowOnError>'`
*/
export function buildResultType({ node, types, returnType }: { node: ast.OperationNode; types: OperationTypeNames; returnType: ReturnTypeOption }): string {
const generics = buildRequestResultGenerics({ node, types })
return returnType === 'data' ? `UnwrappedResult<${generics}>` : `RequestResult<${generics}>`
}
9 changes: 8 additions & 1 deletion internals/client/src/builders/returnStatement.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,15 @@ const node = ast.factory.createOperation({
describe('buildReturnStatement', () => {
test('forwards the call config and casts to the operation RequestResult', () => {
const callConfig = "{ method: 'POST', url: '/pet', ...config }"
expect(buildReturnStatement({ node, types: resolverTs, callConfig })).toBe(
expect(buildReturnStatement({ node, types: resolverTs, callConfig, returnType: 'full' })).toBe(
"return request({ method: 'POST', url: '/pet', ...config }) as Promise<RequestResult<AddPetResponses, ThrowOnError>>",
)
})

test('routes the call through unwrapResult when returnType is data', () => {
const callConfig = "{ method: 'POST', url: '/pet', ...config }"
expect(buildReturnStatement({ node, types: resolverTs, callConfig, returnType: 'data' })).toBe(
"return unwrapResult(request({ method: 'POST', url: '/pet', ...config }), config.throwOnError) as Promise<UnwrappedResult<AddPetResponses, ThrowOnError>>",
)
})
})
Loading
Loading