diff --git a/apps/content/.vitepress/config.ts b/apps/content/.vitepress/config.ts index cb7155197..8e870d1eb 100644 --- a/apps/content/.vitepress/config.ts +++ b/apps/content/.vitepress/config.ts @@ -141,6 +141,7 @@ export default defineConfig({ collapsed: true, items: [ { text: 'Dedupe Middleware', link: '/docs/best-practices/dedupe-middleware' }, + { text: 'No Throw Literal', link: '/docs/best-practices/no-throw-literal' }, ], }, { diff --git a/apps/content/docs/best-practices/no-throw-literal.md b/apps/content/docs/best-practices/no-throw-literal.md new file mode 100644 index 000000000..52e46e877 --- /dev/null +++ b/apps/content/docs/best-practices/no-throw-literal.md @@ -0,0 +1,57 @@ +--- +title: No Throw Literal +description: Always throw `Error` instances instead of literal values. +--- + +# No Throw Literal + +In JavaScript, you can throw any value, but it's best to throw only `Error` instances. + +```ts +// eslint-disable-next-line no-throw-literal +throw 'error' // ✗ avoid +throw new Error('error') // ✓ recommended +``` + +:::info +oRPC treats thrown `Error` instances as best practice by default, as recommended by the [JavaScript Standard Style](https://standardjs.com/rules.html#throw-new-error-old-style). +::: + +## Configuration + +Customize oRPC's behavior by setting `throwableError` in the `Registry`: + +```ts +declare module '@orpc/server' { // or '@orpc/contract', or '@orpc/client' + interface Registry { + throwableError: Error // [!code highlight] + } +} +``` + +:::info +Avoid using `any` or `unknown` for `throwableError` because doing so prevents the client from inferring [type-safe errors](/docs/client/error-handling#using-safe-and-isdefinederror). Instead, use `null | undefined | {}` (equivalent to `unknown`) for stricter error type inference. +::: + +:::tip +If you configure `throwableError` as `null | undefined | {}`, adjust your code to check the `success` property: + +```ts +const { error, data, success } = await safe(client('input')) + +if (!success) { + if (isDefinedError(error)) { + // handle type-safe error + } + // handle other errors +} +else { + // handle success +} +``` + +::: + +## Bonus + +If you use ESLint, enable the [no-throw-literal](https://eslint.org/docs/rules/no-throw-literal) rule to enforce throwing only `Error` instances. diff --git a/packages/client/src/adapters/standard/link.ts b/packages/client/src/adapters/standard/link.ts index 0be31a401..9d815ac89 100644 --- a/packages/client/src/adapters/standard/link.ts +++ b/packages/client/src/adapters/standard/link.ts @@ -1,4 +1,4 @@ -import type { Interceptor } from '@orpc/shared' +import type { Interceptor, ThrowableError } from '@orpc/shared' import type { StandardLazyResponse, StandardRequest } from '@orpc/standard-server' import type { ClientContext, ClientLink, ClientOptions } from '../../types' import type { StandardLinkClient, StandardLinkCodec } from './types' @@ -11,8 +11,8 @@ export interface StandardLinkPlugin { } export interface StandardLinkOptions { - interceptors?: Interceptor<{ path: readonly string[], input: unknown, options: ClientOptions }, unknown, unknown>[] - clientInterceptors?: Interceptor<{ request: StandardRequest }, StandardLazyResponse, unknown>[] + interceptors?: Interceptor<{ path: readonly string[], input: unknown, options: ClientOptions }, unknown, ThrowableError>[] + clientInterceptors?: Interceptor<{ request: StandardRequest }, StandardLazyResponse, ThrowableError>[] plugins?: StandardLinkPlugin[] } diff --git a/packages/client/src/index.ts b/packages/client/src/index.ts index 956b8150d..8a7c52e86 100644 --- a/packages/client/src/index.ts +++ b/packages/client/src/index.ts @@ -6,4 +6,5 @@ export * from './types' export * from './utils' export { onError, onFinish, onStart, onSuccess } from '@orpc/shared' +export type { Registry, ThrowableError } from '@orpc/shared' export { ErrorEvent } from '@orpc/standard-server' diff --git a/packages/client/src/types.ts b/packages/client/src/types.ts index 6b946f8dc..44a1f3533 100644 --- a/packages/client/src/types.ts +++ b/packages/client/src/types.ts @@ -15,9 +15,9 @@ export type ClientRest = Record] : [input: TInput, options: FriendlyClientOptions] -export type ClientPromiseResult = PromiseWithError +export type ClientPromiseResult = PromiseWithError -export interface Client { +export interface Client { (...rest: ClientRest): ClientPromiseResult } diff --git a/packages/client/src/utils.test-d.ts b/packages/client/src/utils.test-d.ts index 895fd6459..01c08030d 100644 --- a/packages/client/src/utils.test-d.ts +++ b/packages/client/src/utils.test-d.ts @@ -7,9 +7,9 @@ describe('safe', async () => { const client = {} as Client> it('tuple style', async () => { - const [error, data, isDefined] = await safe(client('123')) + const [error, data, isDefined, success] = await safe(client('123')) - if (error) { + if (error || !success) { expectTypeOf(error).toEqualTypeOf>() expectTypeOf(data).toEqualTypeOf() expectTypeOf(isDefined).toEqualTypeOf() @@ -33,9 +33,9 @@ describe('safe', async () => { }) it('object style', async () => { - const { error, data, isDefined } = await safe(client('123')) + const { error, data, isDefined, success } = await safe(client('123')) - if (error) { + if (error || !success) { expectTypeOf(error).toEqualTypeOf>() expectTypeOf(data).toEqualTypeOf() expectTypeOf(isDefined).toEqualTypeOf() @@ -57,4 +57,11 @@ describe('safe', async () => { expectTypeOf(isDefined).toEqualTypeOf() } }) + + it('can catch Promise', async () => { + const { error, data } = await safe({} as Promise) + + expectTypeOf(error).toEqualTypeOf() + expectTypeOf(data).toEqualTypeOf() + }) }) diff --git a/packages/client/src/utils.test.ts b/packages/client/src/utils.test.ts index 9e1d60777..7d3cdbcfc 100644 --- a/packages/client/src/utils.test.ts +++ b/packages/client/src/utils.test.ts @@ -3,23 +3,23 @@ import { resolveFriendlyClientOptions, safe } from './utils' it('safe', async () => { const r1 = await safe(Promise.resolve(1)) - expect([...r1]).toEqual([null, 1, false]) - expect({ ...r1 }).toEqual(expect.objectContaining({ error: null, data: 1, isDefined: false })) + expect([...r1]).toEqual([null, 1, false, true]) + expect({ ...r1 }).toEqual(expect.objectContaining({ error: null, data: 1, isDefined: false, success: true })) const e2 = new Error('error') const r2 = await safe(Promise.reject(e2)) - expect([...r2]).toEqual([e2, undefined, false]) - expect({ ...r2 }).toEqual(expect.objectContaining({ error: e2, data: undefined, isDefined: false })) + expect([...r2]).toEqual([e2, undefined, false, false]) + expect({ ...r2 }).toEqual(expect.objectContaining({ error: e2, data: undefined, isDefined: false, success: false })) const e3 = new ORPCError('BAD_GATEWAY', { defined: true }) const r3 = await safe(Promise.reject(e3)) - expect([...r3]).toEqual([e3, undefined, true]) - expect({ ...r3 }).toEqual(expect.objectContaining({ error: e3, data: undefined, isDefined: true })) + expect([...r3]).toEqual([e3, undefined, true, false]) + expect({ ...r3 }).toEqual(expect.objectContaining({ error: e3, data: undefined, isDefined: true, success: false })) const e4 = new ORPCError('BAD_GATEWAY') const r4 = await safe(Promise.reject(e4)) - expect([...r4]).toEqual([e4, undefined, false]) - expect({ ...r4 }).toEqual(expect.objectContaining({ error: e4, data: undefined, isDefined: false })) + expect([...r4]).toEqual([e4, undefined, false, false]) + expect({ ...r4 }).toEqual(expect.objectContaining({ error: e4, data: undefined, isDefined: false, success: false })) }) it('resolveFriendlyClientOptions', () => { diff --git a/packages/client/src/utils.ts b/packages/client/src/utils.ts index 2f44997ca..a07951534 100644 --- a/packages/client/src/utils.ts +++ b/packages/client/src/utils.ts @@ -1,21 +1,22 @@ +import type { ThrowableError } from '@orpc/shared' import type { ORPCError } from './error' import type { ClientContext, ClientOptions, ClientPromiseResult, FriendlyClientOptions } from './types' import { isDefinedError } from './error' -export type SafeResult = - | [error: null, data: TOutput, isDefined: false] - & { error: null, data: TOutput, isDefined: false } - | [error: Exclude>, data: undefined, isDefined: false] - & { error: Exclude>, data: undefined, isDefined: false } - | [error: Extract>, data: undefined, isDefined: true] - & { error: Extract>, data: undefined, isDefined: true } +export type SafeResult = + | [error: null, data: TOutput, isDefined: false, success: true] + & { error: null, data: TOutput, isDefined: false, success: true } + | [error: Exclude>, data: undefined, isDefined: false, success: false] + & { error: Exclude>, data: undefined, isDefined: false, success: false } + | [error: Extract>, data: undefined, isDefined: true, success: false] + & { error: Extract>, data: undefined, isDefined: true, success: false } -export async function safe(promise: ClientPromiseResult): Promise> { +export async function safe(promise: ClientPromiseResult): Promise> { try { const output = await promise return Object.assign( - [null, output, false] satisfies [null, TOutput, false], - { error: null, data: output, isDefined: false as const }, + [null, output, false, true] satisfies [null, TOutput, false, true], + { error: null, data: output, isDefined: false as const, success: true as const }, ) } catch (e) { @@ -23,14 +24,14 @@ export async function safe(promise: ClientPromise if (isDefinedError(error)) { return Object.assign( - [error, undefined, true] satisfies [typeof error, undefined, true], - { error, data: undefined, isDefined: true as const }, + [error, undefined, true, false] satisfies [typeof error, undefined, true, false], + { error, data: undefined, isDefined: true as const, success: false as const }, ) } return Object.assign( - [error as Exclude>, undefined, false] satisfies [Exclude>, undefined, false], - { error: error as Exclude>, data: undefined, isDefined: false as const }, + [error as Exclude>, undefined, false, false] satisfies [Exclude>, undefined, false, false], + { error: error as Exclude>, data: undefined, isDefined: false as const, success: false as const }, ) } } diff --git a/packages/contract/src/error.ts b/packages/contract/src/error.ts index beb5c15c0..1970bb5f2 100644 --- a/packages/contract/src/error.ts +++ b/packages/contract/src/error.ts @@ -1,4 +1,5 @@ import type { ORPCError, ORPCErrorCode } from '@orpc/client' +import type { ThrowableError } from '@orpc/shared' import type { AnySchema, InferSchemaOutput, Schema, SchemaIssue } from './schema' export interface ValidationErrorOptions extends ErrorOptions { @@ -40,4 +41,4 @@ export type ORPCErrorFromErrorMap = { : never }[keyof TErrorMap] -export type ErrorFromErrorMap = Error | ORPCErrorFromErrorMap +export type ErrorFromErrorMap = ORPCErrorFromErrorMap | ThrowableError diff --git a/packages/contract/src/index.ts b/packages/contract/src/index.ts index a738da988..66121bbcc 100644 --- a/packages/contract/src/index.ts +++ b/packages/contract/src/index.ts @@ -15,3 +15,4 @@ export * from './router-utils' export * from './schema' export { ORPCError } from '@orpc/client' +export type { Registry, ThrowableError } from '@orpc/shared' diff --git a/packages/react-query/src/procedure-utils.ts b/packages/react-query/src/procedure-utils.ts index 811529483..fd05ac2af 100644 --- a/packages/react-query/src/procedure-utils.ts +++ b/packages/react-query/src/procedure-utils.ts @@ -4,7 +4,7 @@ import type { InfiniteData } from '@tanstack/react-query' import type { InfiniteOptionsBase, InfiniteOptionsIn, MutationOptions, MutationOptionsIn, QueryOptionsBase, QueryOptionsIn } from './types' import { buildKey } from './key' -export interface ProcedureUtils { +export interface ProcedureUtils { call: Client queryOptions( @@ -26,7 +26,7 @@ export interface CreateProcedureUtilsOptions { path: string[] } -export function createProcedureUtils( +export function createProcedureUtils( client: Client, options: CreateProcedureUtilsOptions, ): ProcedureUtils { diff --git a/packages/react-query/src/types.ts b/packages/react-query/src/types.ts index b04532268..9a15ae51d 100644 --- a/packages/react-query/src/types.ts +++ b/packages/react-query/src/types.ts @@ -2,30 +2,30 @@ import type { ClientContext } from '@orpc/client' import type { SetOptional } from '@orpc/shared' import type { QueryFunctionContext, QueryKey, UseInfiniteQueryOptions, UseMutationOptions, UseQueryOptions } from '@tanstack/react-query' -export type QueryOptionsIn = +export type QueryOptionsIn = & (undefined extends TInput ? { input?: TInput } : { input: TInput }) & (Record extends TClientContext ? { context?: TClientContext } : { context: TClientContext }) & SetOptional, 'queryKey'> -export interface QueryOptionsBase { +export interface QueryOptionsBase { queryKey: QueryKey queryFn(ctx: QueryFunctionContext): Promise retry?(failureCount: number, error: TError): boolean // this make tanstack can infer the TError type } -export type InfiniteOptionsIn = +export type InfiniteOptionsIn = & { input: (pageParam: TPageParam) => TInput } & (Record extends TClientContext ? { context?: TClientContext } : { context: TClientContext }) & SetOptional, 'queryKey'> -export interface InfiniteOptionsBase { +export interface InfiniteOptionsBase { queryKey: QueryKey queryFn(ctx: QueryFunctionContext): Promise retry?(failureCount: number, error: TError): boolean // this make tanstack can infer the TError type } -export type MutationOptionsIn = +export type MutationOptionsIn = & (Record extends TClientContext ? { context?: TClientContext } : { context: TClientContext }) & MutationOptions -export type MutationOptions = UseMutationOptions +export type MutationOptions = UseMutationOptions diff --git a/packages/react/src/hooks/action-hooks.ts b/packages/react/src/hooks/action-hooks.ts index 4a074a864..22d1a2abd 100644 --- a/packages/react/src/hooks/action-hooks.ts +++ b/packages/react/src/hooks/action-hooks.ts @@ -4,24 +4,24 @@ import { ORPCError, safe } from '@orpc/client' import { intercept, type Interceptor, toArray } from '@orpc/shared' import { useCallback, useMemo, useState } from 'react' -export interface UseServerActionOptions { +export interface UseServerActionOptions { interceptors?: Interceptor<{ input: TInput }, TOutput, TError>[] } -export interface UseServerActionExecuteOptions extends Pick, 'interceptors'> { +export interface UseServerActionExecuteOptions extends Pick, 'interceptors'> { } -export type UseServerActionExecuteRest = +export type UseServerActionExecuteRest = undefined extends TInput ? [input?: TInput, options?: UseServerActionExecuteOptions] : [input: TInput, options?: UseServerActionExecuteOptions] -export interface UseServerActionResultBase { +export interface UseServerActionResultBase { reset: () => void execute: (...rest: UseServerActionExecuteRest) => Promise> } -export interface UseServerActionIdleResult extends UseServerActionResultBase { +export interface UseServerActionIdleResult extends UseServerActionResultBase { input: undefined data: undefined error: null @@ -33,7 +33,7 @@ export interface UseServerActionIdleResult extends UseServerActionResultBase { +export interface UseServerActionPendingResult extends UseServerActionResultBase { input: TInput data: undefined error: null @@ -45,7 +45,7 @@ export interface UseServerActionPendingResult extends UseServerActionResultBase { +export interface UseServerActionSuccessResult extends UseServerActionResultBase { input: TInput data: TOutput error: null @@ -57,7 +57,7 @@ export interface UseServerActionSuccessResult extends UseServerActionResultBase { +export interface UseServerActionErrorResult extends UseServerActionResultBase { input: TInput data: undefined error: TError diff --git a/packages/server/src/adapters/fetch/handler.ts b/packages/server/src/adapters/fetch/handler.ts index 4672a5aa5..3f6d9c535 100644 --- a/packages/server/src/adapters/fetch/handler.ts +++ b/packages/server/src/adapters/fetch/handler.ts @@ -1,7 +1,8 @@ +import type { Interceptor, MaybeOptionalOptions, ThrowableError } from '@orpc/shared' import type { Context } from '../../context' import type { StandardHandleOptions, StandardHandler, StandardHandlerPlugin } from '../standard' import type { FriendlyStandardHandleOptions } from '../standard/utils' -import { intercept, type Interceptor, type MaybeOptionalOptions, resolveMaybeOptionalOptions, toArray } from '@orpc/shared' +import { intercept, resolveMaybeOptionalOptions, toArray } from '@orpc/shared' import { toFetchResponse, type ToFetchResponseOptions, toStandardLazyRequest } from '@orpc/standard-server-fetch' import { resolveFriendlyStandardHandleOptions } from '../standard/utils' @@ -17,7 +18,7 @@ export interface FetchHandlerInterceptorOptions extends Stand } export interface FetchHandlerOptions extends ToFetchResponseOptions { - adapterInterceptors?: Interceptor, FetchHandleResult, unknown >[] + adapterInterceptors?: Interceptor, FetchHandleResult, ThrowableError>[] plugins?: FetchHandlerPlugin[] } diff --git a/packages/server/src/adapters/node/handler.ts b/packages/server/src/adapters/node/handler.ts index 72d767ed0..782e02020 100644 --- a/packages/server/src/adapters/node/handler.ts +++ b/packages/server/src/adapters/node/handler.ts @@ -1,7 +1,8 @@ +import type { Interceptor, MaybeOptionalOptions, ThrowableError } from '@orpc/shared' import type { NodeHttpRequest, NodeHttpResponse, SendStandardResponseOptions } from '@orpc/standard-server-node' import type { Context } from '../../context' import type { StandardHandleOptions, StandardHandler, StandardHandlerPlugin } from '../standard' -import { intercept, type Interceptor, type MaybeOptionalOptions, resolveMaybeOptionalOptions, toArray } from '@orpc/shared' +import { intercept, resolveMaybeOptionalOptions, toArray } from '@orpc/shared' import { sendStandardResponse, toStandardLazyRequest } from '@orpc/standard-server-node' import { type FriendlyStandardHandleOptions, resolveFriendlyStandardHandleOptions } from '../standard/utils' @@ -18,7 +19,7 @@ export interface NodeHttpHandlerInterceptorOptions extends St } export interface NodeHttpHandlerOptions extends SendStandardResponseOptions { - adapterInterceptors?: Interceptor, NodeHttpHandleResult, unknown >[] + adapterInterceptors?: Interceptor, NodeHttpHandleResult, ThrowableError>[] plugins?: NodeHttpHandlerPlugin[] } diff --git a/packages/server/src/adapters/standard/handler.ts b/packages/server/src/adapters/standard/handler.ts index 202a1ebf5..f719b13e8 100644 --- a/packages/server/src/adapters/standard/handler.ts +++ b/packages/server/src/adapters/standard/handler.ts @@ -1,6 +1,6 @@ import type { HTTPPath } from '@orpc/client' import type { AnySchema, ErrorFromErrorMap, InferSchemaOutput, Meta } from '@orpc/contract' -import type { Interceptor } from '@orpc/shared' +import type { Interceptor, ThrowableError } from '@orpc/shared' import type { StandardLazyRequest, StandardResponse } from '@orpc/standard-server' import type { Context } from '../../context' import type { ProcedureClientInterceptorOptions } from '../../procedure-client' @@ -31,12 +31,12 @@ export interface StandardHandlerOptions { /** * Interceptors at the request level, helpful when you want catch errors */ - interceptors?: Interceptor, StandardHandleResult, unknown>[] + interceptors?: Interceptor, StandardHandleResult, ThrowableError>[] /** * Interceptors at the root level, helpful when you want override the request/response */ - rootInterceptors?: Interceptor, StandardHandleResult, unknown>[] + rootInterceptors?: Interceptor, StandardHandleResult, ThrowableError>[] /** * diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts index 307df9885..afdecced4 100644 --- a/packages/server/src/index.ts +++ b/packages/server/src/index.ts @@ -37,4 +37,5 @@ export type { Schema, } from '@orpc/contract' export { onError, onFinish, onStart, onSuccess } from '@orpc/shared' +export type { Registry, ThrowableError } from '@orpc/shared' export { getEventMeta, withEventMeta } from '@orpc/standard-server' diff --git a/packages/server/src/procedure-action.ts b/packages/server/src/procedure-action.ts index b6deefa47..219316728 100644 --- a/packages/server/src/procedure-action.ts +++ b/packages/server/src/procedure-action.ts @@ -1,10 +1,11 @@ import type { Client, ORPCError, ORPCErrorJSON } from '@orpc/client' import type { AnySchema, ErrorFromErrorMap, ErrorMap, InferSchemaInput, InferSchemaOutput } from '@orpc/contract' +import type { ThrowableError } from '@orpc/shared' import { toORPCError } from '@orpc/client' -export type ActionableError = T extends ORPCError ? ORPCErrorJSON & { defined: true } : ORPCErrorJSON & { defined: false } +export type ActionableError = T extends ORPCError ? ORPCErrorJSON & { defined: true } : ORPCErrorJSON & { defined: false } -export type UnactionableError = T extends { defined: true } & ORPCErrorJSON ? ORPCError : Error +export type UnactionableError = T extends { defined: true } & ORPCErrorJSON ? ORPCError : ThrowableError export type ActionableClientRest = | [input: TInput] @@ -26,7 +27,7 @@ export type ProcedureActionableClient< ActionableError> > -export function createActionableClient( +export function createActionableClient( client: Client, TInput, TOutput, TError>, ): ActionableClient> { const action = async (input: TInput) => { diff --git a/packages/server/src/procedure-client.test.ts b/packages/server/src/procedure-client.test.ts index 41dc53ac4..b4332d0be 100644 --- a/packages/server/src/procedure-client.test.ts +++ b/packages/server/src/procedure-client.test.ts @@ -444,12 +444,6 @@ describe.each(procedureCases)('createProcedureClient - case %s', async (_, proce describe('error validation', () => { const client = createProcedureClient(procedure) - it('transform non-error to error', () => { - handler.mockRejectedValueOnce('non-error') - - expect(client({ val: '123' })).rejects.toThrow('Unknown error') - }) - it('throw non-ORPC Error right away', () => { const e1 = new Error('non-ORPC Error') handler.mockRejectedValueOnce(e1) diff --git a/packages/server/src/procedure-client.ts b/packages/server/src/procedure-client.ts index e03305e00..1281a7f2b 100644 --- a/packages/server/src/procedure-client.ts +++ b/packages/server/src/procedure-client.ts @@ -5,7 +5,7 @@ import type { MiddlewareNextFn } from './middleware' import type { AnyProcedure, Procedure, ProcedureHandlerOptions } from './procedure' import { ORPCError } from '@orpc/client' import { type AnySchema, type ErrorFromErrorMap, type ErrorMap, type InferSchemaInput, type InferSchemaOutput, type Meta, ValidationError } from '@orpc/contract' -import { intercept, toError, value } from '@orpc/shared' +import { intercept, value } from '@orpc/shared' import { type Context, mergeCurrentContext } from './context' import { createORPCErrorConstructorMap, type ORPCErrorConstructorMap, validateORPCError } from './error' import { unlazy } from './lazy' @@ -109,7 +109,7 @@ export function createProcedureClient< } catch (e) { if (!(e instanceof ORPCError)) { - throw toError(e) + throw e } const validated = await validateORPCError(procedure['~orpc'].errorMap, e) diff --git a/packages/shared/src/error.test.ts b/packages/shared/src/error.test.ts deleted file mode 100644 index dbf78aa5f..000000000 --- a/packages/shared/src/error.test.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { toError } from './error' - -it('toError', () => { - const error = new Error('hi') - - expect(toError(error)).toBe(error) - - const e2 = { message: 'hi' } - expect(toError(e2)).toBeInstanceOf(Error) - expect(toError(e2).message).toEqual('Unknown error') - expect(toError(e2).cause).toEqual(e2) -}) diff --git a/packages/shared/src/error.ts b/packages/shared/src/error.ts deleted file mode 100644 index bdcc7c823..000000000 --- a/packages/shared/src/error.ts +++ /dev/null @@ -1,7 +0,0 @@ -export function toError(error: unknown): Error { - if (error instanceof Error) { - return error - } - - return new Error('Unknown error', { cause: error }) -} diff --git a/packages/shared/src/index.ts b/packages/shared/src/index.ts index d89b9ba11..74ba8aad0 100644 --- a/packages/shared/src/index.ts +++ b/packages/shared/src/index.ts @@ -1,7 +1,6 @@ export * from './args' export * from './array' export * from './chain' -export * from './error' export * from './function' export * from './interceptor' export * from './iterator' diff --git a/packages/shared/src/interceptor.ts b/packages/shared/src/interceptor.ts index 067a9b199..26ed5a80c 100644 --- a/packages/shared/src/interceptor.ts +++ b/packages/shared/src/interceptor.ts @@ -1,5 +1,5 @@ import type { Promisable } from 'type-fest' -import type { PromiseWithError } from './types' +import type { PromiseWithError, ThrowableError } from './types' export type InterceptableOptions = Record @@ -47,7 +47,7 @@ export function onSuccess */ export function onError( callback: NoInfer<( - error: ReturnType extends PromiseWithError ? E : unknown, + error: ReturnType extends PromiseWithError ? E : ThrowableError, options: TOptions, ...rest: TRest ) => Promisable>, @@ -72,7 +72,7 @@ export function onFinish( callback: NoInfer<( state: OnFinishState< Awaited>, - ReturnType extends PromiseWithError ? E : unknown + ReturnType extends PromiseWithError ? E : ThrowableError >, options: TOptions, ...rest: TRest diff --git a/packages/shared/src/types.ts b/packages/shared/src/types.ts index 8e9d78950..60eab6182 100644 --- a/packages/shared/src/types.ts +++ b/packages/shared/src/types.ts @@ -3,3 +3,14 @@ export type SetOptional = Omit & Partial> export type IntersectPick = Pick export type PromiseWithError = Promise & { __error?: { type: TError } } + +/** + * The place where you can config the orpc types. + * + * - `throwableError` the error type that represent throwable errors should be `Error` or `null | undefined | {}` if you want more strict. + */ +export interface Registry { + +} + +export type ThrowableError = Registry extends { throwableError: infer T } ? T : Error diff --git a/packages/solid-query/src/procedure-utils.ts b/packages/solid-query/src/procedure-utils.ts index 544292578..6b438d869 100644 --- a/packages/solid-query/src/procedure-utils.ts +++ b/packages/solid-query/src/procedure-utils.ts @@ -4,7 +4,7 @@ import type { InfiniteData } from '@tanstack/solid-query' import type { InfiniteOptionsBase, InfiniteOptionsIn, MutationOptions, MutationOptionsIn, QueryOptionsBase, QueryOptionsIn } from './types' import { buildKey } from './key' -export interface ProcedureUtils { +export interface ProcedureUtils { call: Client queryOptions( @@ -26,7 +26,7 @@ export interface CreateProcedureUtilsOptions { path: string[] } -export function createProcedureUtils( +export function createProcedureUtils( client: Client, options: CreateProcedureUtilsOptions, ): ProcedureUtils { diff --git a/packages/solid-query/src/types.ts b/packages/solid-query/src/types.ts index 70b25eb22..da788b9cf 100644 --- a/packages/solid-query/src/types.ts +++ b/packages/solid-query/src/types.ts @@ -8,30 +8,30 @@ import type { SolidQueryOptions, } from '@tanstack/solid-query' -export type QueryOptionsIn = +export type QueryOptionsIn = & (undefined extends TInput ? { input?: TInput } : { input: TInput }) & (Record extends TClientContext ? { context?: TClientContext } : { context: TClientContext }) & SetOptional, 'queryKey'> -export interface QueryOptionsBase { +export interface QueryOptionsBase { queryKey: QueryKey queryFn(ctx: QueryFunctionContext): Promise retry?(failureCount: number, error: TError): boolean // this make tanstack can infer the TError type } -export type InfiniteOptionsIn = +export type InfiniteOptionsIn = & { input: (pageParam: TPageParam) => TInput } & (Record extends TClientContext ? { context?: TClientContext } : { context: TClientContext }) & SetOptional, 'queryKey'> -export interface InfiniteOptionsBase { +export interface InfiniteOptionsBase { queryKey: QueryKey queryFn(ctx: QueryFunctionContext): Promise retry?(failureCount: number, error: TError): boolean // this make tanstack can infer the TError type } -export type MutationOptionsIn = +export type MutationOptionsIn = & (Record extends TClientContext ? { context?: TClientContext } : { context: TClientContext }) & MutationOptions -export type MutationOptions = SolidMutationOptions +export type MutationOptions = SolidMutationOptions diff --git a/packages/svelte-query/src/procedure-utils.ts b/packages/svelte-query/src/procedure-utils.ts index afc1fd728..c6c1fe2c7 100644 --- a/packages/svelte-query/src/procedure-utils.ts +++ b/packages/svelte-query/src/procedure-utils.ts @@ -4,7 +4,7 @@ import type { InfiniteData } from '@tanstack/svelte-query' import type { InfiniteOptionsBase, InfiniteOptionsIn, MutationOptions, MutationOptionsIn, QueryOptionsBase, QueryOptionsIn } from './types' import { buildKey } from './key' -export interface ProcedureUtils { +export interface ProcedureUtils { call: Client queryOptions( @@ -26,7 +26,7 @@ export interface CreateProcedureUtilsOptions { path: string[] } -export function createProcedureUtils( +export function createProcedureUtils( client: Client, options: CreateProcedureUtilsOptions, ): ProcedureUtils { diff --git a/packages/svelte-query/src/types.ts b/packages/svelte-query/src/types.ts index 9a1ee87b5..34636f545 100644 --- a/packages/svelte-query/src/types.ts +++ b/packages/svelte-query/src/types.ts @@ -8,30 +8,30 @@ import type { QueryKey, } from '@tanstack/svelte-query' -export type QueryOptionsIn = +export type QueryOptionsIn = & (undefined extends TInput ? { input?: TInput } : { input: TInput }) & (Record extends TClientContext ? { context?: TClientContext } : { context: TClientContext }) & SetOptional, 'queryKey'> -export interface QueryOptionsBase { +export interface QueryOptionsBase { queryKey: QueryKey queryFn(ctx: QueryFunctionContext): Promise retry?(failureCount: number, error: TError): boolean // this make tanstack can infer the TError type } -export type InfiniteOptionsIn = +export type InfiniteOptionsIn = & { input: (pageParam: TPageParam) => TInput } & (Record extends TClientContext ? { context?: TClientContext } : { context: TClientContext }) & SetOptional, 'queryKey'> -export interface InfiniteOptionsBase { +export interface InfiniteOptionsBase { queryKey: QueryKey queryFn(ctx: QueryFunctionContext): Promise retry?(failureCount: number, error: TError): boolean // this make tanstack can infer the TError type } -export type MutationOptionsIn = +export type MutationOptionsIn = & (Record extends TClientContext ? { context?: TClientContext } : { context: TClientContext }) & MutationOptions -export type MutationOptions = CreateMutationOptions +export type MutationOptions = CreateMutationOptions diff --git a/packages/vue-colada/src/procedure-utils.ts b/packages/vue-colada/src/procedure-utils.ts index e76eae8ec..eaefb253a 100644 --- a/packages/vue-colada/src/procedure-utils.ts +++ b/packages/vue-colada/src/procedure-utils.ts @@ -5,7 +5,7 @@ import type { MutationOptions, MutationOptionsIn, QueryOptions, QueryOptionsIn } import { computed, toValue } from 'vue' import { buildKey } from './key' -export interface ProcedureUtils { +export interface ProcedureUtils { call: Client queryOptions( @@ -25,7 +25,7 @@ export interface CreateProcedureUtilsOptions { path: string[] } -export function createProcedureUtils( +export function createProcedureUtils( client: Client, options: CreateProcedureUtilsOptions, ): ProcedureUtils { diff --git a/packages/vue-colada/src/types.ts b/packages/vue-colada/src/types.ts index 29b63225e..3e2a78493 100644 --- a/packages/vue-colada/src/types.ts +++ b/packages/vue-colada/src/types.ts @@ -5,15 +5,15 @@ import type { MaybeRefOrGetter } from 'vue' export type UseQueryFnContext = Parameters['query']>[0] -export type QueryOptionsIn = +export type QueryOptionsIn = & (undefined extends TInput ? { input?: MaybeRefOrGetter } : { input: MaybeRefOrGetter }) & (Record extends TClientContext ? { context?: MaybeRefOrGetter } : { context: MaybeRefOrGetter }) & SetOptional, 'key' | 'query'> -export type QueryOptions = UseQueryOptions +export type QueryOptions = UseQueryOptions -export type MutationOptionsIn> = +export type MutationOptionsIn> = & (Record extends TClientContext ? { context?: MaybeRefOrGetter } : { context: MaybeRefOrGetter }) & SetOptional, 'mutation'> -export type MutationOptions> = UseMutationOptions +export type MutationOptions> = UseMutationOptions diff --git a/packages/vue-query/src/procedure-utils.ts b/packages/vue-query/src/procedure-utils.ts index b794ea9d5..aa176b004 100644 --- a/packages/vue-query/src/procedure-utils.ts +++ b/packages/vue-query/src/procedure-utils.ts @@ -6,7 +6,7 @@ import { computed } from 'vue' import { buildKey } from './key' import { unrefDeep } from './utils' -export interface ProcedureUtils { +export interface ProcedureUtils { call: Client queryOptions( @@ -28,7 +28,7 @@ export interface CreateProcedureUtilsOptions { path: string[] } -export function createProcedureUtils( +export function createProcedureUtils( client: Client, options: CreateProcedureUtilsOptions, ): ProcedureUtils { diff --git a/packages/vue-query/src/types.ts b/packages/vue-query/src/types.ts index 6ad3b8794..36a01b840 100644 --- a/packages/vue-query/src/types.ts +++ b/packages/vue-query/src/types.ts @@ -15,7 +15,7 @@ export type MaybeRefDeep = MaybeRef< : T > -export type QueryOptionsIn = +export type QueryOptionsIn = & (undefined extends TInput ? { input?: MaybeRefDeep } : { input: MaybeRefDeep }) & (Record extends TClientContext ? { context?: MaybeRefDeep } : { context: MaybeRefDeep }) & { @@ -28,28 +28,28 @@ export type QueryOptionsIn } -export interface QueryOptionsBase { +export interface QueryOptionsBase { queryKey: ComputedRef queryFn(ctx: QueryFunctionContext): Promise retry?(failureCount: number, error: TError): boolean // this help tanstack can infer TError } -export type InfiniteOptionsIn = +export type InfiniteOptionsIn = & { input: (pageParam: TPageParam) => MaybeRefDeep } & (Record extends TClientContext ? { context?: MaybeRefDeep } : { context: MaybeRefDeep }) & SetOptional, 'queryKey'> -export interface InfiniteOptionsBase { +export interface InfiniteOptionsBase { queryKey: ComputedRef queryFn(ctx: QueryFunctionContext): Promise retry?(failureCount: number, error: TError): boolean // this help tanstack can infer TError } -export type MutationOptionsIn = +export type MutationOptionsIn = & (Record extends TClientContext ? { context?: TClientContext } : { context: TClientContext }) & MutationOptions -export type MutationOptions = +export type MutationOptions = { [P in keyof MutationObserverOptions]: MaybeRefDeep[P]> } & {