Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion packages/client/src/types.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import type { PromiseWithError } from '@orpc/shared'

export type HTTPPath = `/${string}`
export type HTTPMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH'

Expand All @@ -13,7 +15,7 @@ export type ClientRest<TClientContext extends ClientContext, TInput> = Record<ne
: [input: TInput, options?: FriendlyClientOptions<TClientContext>]
: [input: TInput, options: FriendlyClientOptions<TClientContext>]

export type ClientPromiseResult<TOutput, TError extends Error> = Promise<TOutput> & { __error?: { type: TError } }
export type ClientPromiseResult<TOutput, TError extends Error> = PromiseWithError<TOutput, TError>

export interface Client<TClientContext extends ClientContext, TInput, TOutput, TError extends Error> {
(...rest: ClientRest<TClientContext, TInput>): ClientPromiseResult<TOutput, TError>
Expand Down
9 changes: 5 additions & 4 deletions packages/shared/src/interceptor.test-d.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import type { Interceptor } from './interceptor'
import type { PromiseWithError } from './types'
import { onError, onFinish, onStart, onSuccess } from './interceptor'

it('onStart', () => {
const interceptor: Interceptor<{ foo: string }, 'success', 'error'> = onStart((options) => {
expectTypeOf(options.foo).toEqualTypeOf<string>()
expectTypeOf(options.next).toBeCallableWith<[options?: { foo: string }]>()
expectTypeOf(options.next()).toEqualTypeOf<Promise<'success'> & { __error?: { type: 'error' } }>()
expectTypeOf(options.next()).toEqualTypeOf<PromiseWithError<'success', 'error'>>()
})
})

Expand All @@ -15,7 +16,7 @@ it('onSuccess', () => {

expectTypeOf(options.foo).toEqualTypeOf<string>()
expectTypeOf(options.next).toBeCallableWith<[options?: { foo: string }]>()
expectTypeOf(options.next()).toEqualTypeOf<Promise<'success'> & { __error?: { type: 'error' } }>()
expectTypeOf(options.next()).toEqualTypeOf<PromiseWithError<'success', 'error'>>()
})
})

Expand All @@ -25,7 +26,7 @@ it('onError', () => {

expectTypeOf(options.foo).toEqualTypeOf<string>()
expectTypeOf(options.next).toBeCallableWith<[options?: { foo: string }]>()
expectTypeOf(options.next()).toEqualTypeOf<Promise<'success'> & { __error?: { type: 'error' } }>()
expectTypeOf(options.next()).toEqualTypeOf<PromiseWithError<'success', 'error'>>()
})
})

Expand All @@ -35,6 +36,6 @@ it('onFinish', () => {

expectTypeOf(options.foo).toEqualTypeOf<string>()
expectTypeOf(options.next).toBeCallableWith<[options?: { foo: string }]>()
expectTypeOf(options.next()).toEqualTypeOf<Promise<'success'> & { __error?: { type: 'error' } }>()
expectTypeOf(options.next()).toEqualTypeOf<PromiseWithError<'success', 'error'>>()
})
})
37 changes: 25 additions & 12 deletions packages/shared/src/interceptor.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { Promisable } from 'type-fest'
import type { PromiseWithError } from './types'

export type InterceptableOptions = Record<string, any>

Expand All @@ -7,14 +8,14 @@ export type InterceptorOptions<
TResult,
TError,
> = Omit<TOptions, 'next'> & {
next(options?: TOptions): Promise<TResult> & { __error?: { type: TError } }
next(options?: TOptions): PromiseWithError<TResult, TError>
}

export type Interceptor<
TOptions extends InterceptableOptions,
TResult,
TError,
> = (options: InterceptorOptions<TOptions, TResult, TError>) => Promise<TResult> & { __error?: { type: TError } }
> = (options: InterceptorOptions<TOptions, TResult, TError>) => PromiseWithError<TResult, TError>

/**
* Can used for interceptors or middlewares
Expand Down Expand Up @@ -44,15 +45,19 @@ export function onSuccess<TOptions extends { next(): any }, TRest extends any[]>
/**
* Can used for interceptors or middlewares
*/
export function onError<TError, TOptions extends { next(): any }, TRest extends any[]>(
callback: NoInfer<(error: TError, options: TOptions, ...rest: TRest) => Promisable<void>>,
): (options: TOptions, ...rest: TRest) => Promise<Awaited<ReturnType<TOptions['next']>>> & { __error?: { type: TError } } {
export function onError<TOptions extends { next(): any }, TRest extends any[]>(
callback: NoInfer<(
error: ReturnType<TOptions['next']> extends PromiseWithError<any, infer E> ? E : unknown,
options: TOptions,
...rest: TRest
) => Promisable<void>>,
): (options: TOptions, ...rest: TRest) => Promise<Awaited<ReturnType<TOptions['next']>>> {
return async (options, ...rest) => {
try {
return await options.next()
}
catch (error) {
await callback(error as TError, options, ...rest)
await callback(error as any, options, ...rest)
throw error
}
}
Expand All @@ -63,22 +68,30 @@ export type OnFinishState<TResult, TError> = [TResult, null, 'success'] | [undef
/**
* Can used for interceptors or middlewares
*/
export function onFinish<TError, TOptions extends { next(): any }, TRest extends any[]>(
callback: NoInfer<(state: OnFinishState<Awaited<ReturnType<TOptions['next']>>, TError>, options: TOptions, ...rest: TRest) => Promisable<void>>,
): (options: TOptions, ...rest: TRest) => Promise<Awaited<ReturnType<TOptions['next']>>> & { __error?: { type: TError } } {
let state: OnFinishState<Awaited<ReturnType<TOptions['next']>>, TError> | undefined
export function onFinish<TOptions extends { next(): any }, TRest extends any[]>(
callback: NoInfer<(
state: OnFinishState<
Awaited<ReturnType<TOptions['next']>>,
ReturnType<TOptions['next']> extends PromiseWithError<any, infer E> ? E : unknown
>,
options: TOptions,
...rest: TRest
) => Promisable<void>>,
): (options: TOptions, ...rest: TRest) => Promise<Awaited<ReturnType<TOptions['next']>>> {
let state: any

return async (options, ...rest) => {
try {
const result = await options.next()
state = [result, null, 'success']
return result
}
catch (error) {
state = [undefined, error as TError, 'error']
state = [undefined, error, 'error']
throw error
}
finally {
await callback(state as Exclude<typeof state, undefined>, options, ...rest)
await callback(state, options, ...rest)
}
}
}
Expand Down
9 changes: 8 additions & 1 deletion packages/shared/src/types.test-d.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { IntersectPick, SetOptional } from './types'
import type { IntersectPick, PromiseWithError, SetOptional } from './types'

it('SetOptional', () => {
expectTypeOf<SetOptional<{ a: number }, 'a'>>().toMatchTypeOf<{ a?: number }>()
Expand All @@ -12,3 +12,10 @@ it('IntersectPick', () => {
expectTypeOf<IntersectPick<{ a: number, b: number }, { b: number }>>().toEqualTypeOf<{ b: number }>()
expectTypeOf<IntersectPick<{ a: number }, { b: number }>>().toEqualTypeOf<Empty>()
})

it('PromiseWithError', () => {
type C = PromiseWithError<number | undefined | null, Error | undefined | null>

expectTypeOf<C extends Promise<infer T> ? T : never>().toEqualTypeOf<number | undefined | null>()
expectTypeOf<C extends PromiseWithError<infer T, infer E> ? [T, E] : never>().toEqualTypeOf<[number | undefined | null, Error | undefined | null]>()
})
2 changes: 2 additions & 0 deletions packages/shared/src/types.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
export type SetOptional<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>

export type IntersectPick<T, U> = Pick<T, keyof T & keyof U>

export type PromiseWithError<T, TError> = Promise<T> & { __error?: { type: TError } }