Skip to content

Commit 553adec

Browse files
authored
feat(client)!: update safe utility (#174)
* feat!: update `safe` utility * sync * improve * avoid as any
1 parent 5559bf8 commit 553adec

File tree

8 files changed

+91
-37
lines changed

8 files changed

+91
-37
lines changed

apps/content/docs/client/error-handling.md

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,10 @@ const doSomething = os
2929
})
3030
.callable()
3131

32-
const [data, error] = await safe(doSomething({ id: '123' }))
32+
const [error, data, isDefined] = await safe(doSomething({ id: '123' }))
33+
// or const { error, data, isDefined } = await safe(doSomething({ id: '123' }))
3334

34-
if (isDefinedError(error)) {
35+
if (isDefinedError(error)) { // or isDefined
3536
// handle known error
3637
console.log(error.data.retryAfter)
3738
}
@@ -47,5 +48,8 @@ else {
4748
:::info
4849

4950
- `safe` works like `try/catch`, but can infer error types.
51+
- `safe` supports both tuple `[error, data, isDefined]` and object `{ error, data, isDefined }` styles.
5052
- `isDefinedError` checks if an error originates from `.errors`.
51-
:::
53+
- `isDefined` can replace `isDefinedError`
54+
55+
:::

packages/client/src/utils.test-d.ts

Lines changed: 47 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,24 +3,58 @@ import type { Client, ClientContext } from './types'
33
import { isDefinedError } from './error'
44
import { safe } from './utils'
55

6-
it('safe', async () => {
6+
describe('safe', async () => {
77
const client = {} as Client<ClientContext, string, number, Error | ORPCError<'BAD_GATEWAY', { val: string }>>
88

9-
const [output, error, isDefined] = await safe(client('123'))
9+
it('tuple style', async () => {
10+
const [error, data, isDefined] = await safe(client('123'))
1011

11-
if (!error) {
12-
expectTypeOf(output).toEqualTypeOf<number>()
13-
}
12+
if (error) {
13+
expectTypeOf(error).toEqualTypeOf<Error | ORPCError<'BAD_GATEWAY', { val: string }>>()
14+
expectTypeOf(data).toEqualTypeOf<undefined>()
15+
expectTypeOf(isDefined).toEqualTypeOf<boolean>()
1416

15-
if (isDefined) {
16-
expectTypeOf(error).toEqualTypeOf<ORPCError<'BAD_GATEWAY', { val: string }>>()
17-
}
17+
if (isDefinedError(error)) {
18+
expectTypeOf(error).toEqualTypeOf<ORPCError<'BAD_GATEWAY', { val: string }>>()
19+
}
1820

19-
if (error) {
20-
expectTypeOf(error).toEqualTypeOf<Error | ORPCError<'BAD_GATEWAY', { val: string }>>()
21+
if (isDefined) {
22+
expectTypeOf(error).toEqualTypeOf<ORPCError<'BAD_GATEWAY', { val: string }>>()
23+
}
24+
else {
25+
expectTypeOf(error).toEqualTypeOf<Error>()
26+
}
27+
}
28+
else {
29+
expectTypeOf(error).toEqualTypeOf<null>()
30+
expectTypeOf(data).toEqualTypeOf<number>()
31+
expectTypeOf(isDefined).toEqualTypeOf<false>()
32+
}
33+
})
34+
35+
it('object style', async () => {
36+
const { error, data, isDefined } = await safe(client('123'))
2137

22-
if (isDefinedError(error)) {
23-
expectTypeOf(error).toEqualTypeOf<ORPCError<'BAD_GATEWAY', { val: string }>>()
38+
if (error) {
39+
expectTypeOf(error).toEqualTypeOf<Error | ORPCError<'BAD_GATEWAY', { val: string }>>()
40+
expectTypeOf(data).toEqualTypeOf<undefined>()
41+
expectTypeOf(isDefined).toEqualTypeOf<boolean>()
42+
43+
if (isDefinedError(error)) {
44+
expectTypeOf(error).toEqualTypeOf<ORPCError<'BAD_GATEWAY', { val: string }>>()
45+
}
46+
47+
if (isDefined) {
48+
expectTypeOf(error).toEqualTypeOf<ORPCError<'BAD_GATEWAY', { val: string }>>()
49+
}
50+
else {
51+
expectTypeOf(error).toEqualTypeOf<Error>()
52+
}
53+
}
54+
else {
55+
expectTypeOf(error).toEqualTypeOf<null>()
56+
expectTypeOf(data).toEqualTypeOf<number>()
57+
expectTypeOf(isDefined).toEqualTypeOf<false>()
2458
}
25-
}
59+
})
2660
})

packages/client/src/utils.test.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,21 @@ import { safe } from './utils'
33

44
it('safe', async () => {
55
const r1 = await safe(Promise.resolve(1))
6-
expect(r1).toEqual([1, undefined, false])
6+
expect([...r1]).toEqual([null, 1, false])
7+
expect({ ...r1 }).toEqual(expect.objectContaining({ error: null, data: 1, isDefined: false }))
78

89
const e2 = new Error('error')
910
const r2 = await safe(Promise.reject(e2))
10-
expect(r2).toEqual([undefined, e2, false])
11+
expect([...r2]).toEqual([e2, undefined, false])
12+
expect({ ...r2 }).toEqual(expect.objectContaining({ error: e2, data: undefined, isDefined: false }))
1113

1214
const e3 = new ORPCError('BAD_GATEWAY', { defined: true })
1315
const r3 = await safe(Promise.reject(e3))
14-
expect(r3).toEqual([undefined, e3, true])
16+
expect([...r3]).toEqual([e3, undefined, true])
17+
expect({ ...r3 }).toEqual(expect.objectContaining({ error: e3, data: undefined, isDefined: true }))
1518

1619
const e4 = new ORPCError('BAD_GATEWAY')
1720
const r4 = await safe(Promise.reject(e4))
18-
expect(r4).toEqual([undefined, e4, false])
21+
expect([...r4]).toEqual([e4, undefined, false])
22+
expect({ ...r4 }).toEqual(expect.objectContaining({ error: e4, data: undefined, isDefined: false }))
1923
})

packages/client/src/utils.ts

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,22 +3,34 @@ import type { ClientPromiseResult } from './types'
33
import { isDefinedError } from './error'
44

55
export type SafeResult<TOutput, TError extends Error> =
6-
| [output: TOutput, error: undefined, isDefinedError: false]
7-
| [output: undefined, error: TError, isDefinedError: false]
8-
| [output: undefined, error: Extract<TError, ORPCError<any, any>>, isDefinedError: true]
6+
| [error: null, data: TOutput, isDefined: false]
7+
& { error: null, data: TOutput, isDefined: false }
8+
| [error: Exclude<TError, ORPCError<any, any>>, data: undefined, isDefined: false]
9+
& { error: Exclude<TError, ORPCError<any, any>>, data: undefined, isDefined: false }
10+
| [error: Extract<TError, ORPCError<any, any>>, data: undefined, isDefined: true]
11+
& { error: Extract<TError, ORPCError<any, any>>, data: undefined, isDefined: true }
912

1013
export async function safe<TOutput, TError extends Error>(promise: ClientPromiseResult<TOutput, TError>): Promise<SafeResult<TOutput, TError>> {
1114
try {
1215
const output = await promise
13-
return [output, undefined, false]
16+
return Object.assign(
17+
[null, output, false] satisfies [null, TOutput, false],
18+
{ error: null, data: output, isDefined: false as const },
19+
)
1420
}
1521
catch (e) {
1622
const error = e as TError
1723

1824
if (isDefinedError(error)) {
19-
return [undefined, error, true]
25+
return Object.assign(
26+
[error, undefined, true] satisfies [typeof error, undefined, true],
27+
{ error, data: undefined, isDefined: true as const },
28+
)
2029
}
2130

22-
return [undefined, error, false]
31+
return Object.assign(
32+
[error as Exclude<TError, ORPCError<any, any>>, undefined, false] satisfies [Exclude<TError, ORPCError<any, any>>, undefined, false],
33+
{ error: error as Exclude<TError, ORPCError<any, any>>, data: undefined, isDefined: false as const },
34+
)
2335
}
2436
}

packages/client/tests/e2e.test-d.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,18 +23,18 @@ describe('e2e', () => {
2323
})
2424

2525
it('infer errors', async () => {
26-
const [,error] = await safe(orpc.post.find({ id: '123' }))
26+
const [error] = await safe(orpc.post.find({ id: '123' }))
2727

2828
expectTypeOf(error).toEqualTypeOf<
29-
| undefined
29+
| null
3030
| Error
3131
| ORPCError<'NOT_FOUND', { id: string }>
3232
>()
3333

34-
const [, error2] = await safe(orpc.post.create({ title: 'title' }))
34+
const [error2] = await safe(orpc.post.create({ title: 'title' }))
3535

3636
expectTypeOf(error2).toEqualTypeOf<
37-
| undefined
37+
| null
3838
| Error
3939
| ORPCError<'CONFLICT', { title: string, thumbnail?: File }>
4040
| ORPCError<'FORBIDDEN', { title: string, thumbnail?: File }>

packages/client/tests/e2e.test.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,20 +14,20 @@ describe('e2e', () => {
1414
})
1515

1616
it('on error', async () => {
17-
const [, error, isDefined] = await safe(orpc.post.find({ id: 'NOT_FOUND' }))
17+
const [error,, isDefined] = await safe(orpc.post.find({ id: 'NOT_FOUND' }))
1818

1919
expect(isDefined).toBe(true)
2020
expect(error).toBeInstanceOf(ORPCError)
2121
expect((error as any).data).toEqual({ id: 'NOT_FOUND' })
2222

23-
const [, error2, isDefined2] = await safe(orpc.post.create({ title: 'CONFLICT' }))
23+
const [error2,, isDefined2] = await safe(orpc.post.create({ title: 'CONFLICT' }))
2424

2525
expect(isDefined2).toBe(true)
2626
expect(error2).toBeInstanceOf(ORPCError)
2727
expect((error2 as any).data).toEqual({ title: 'CONFLICT' })
2828

2929
// @ts-expect-error - invalid input
30-
const [, error3, isDefined3] = await safe(orpc.post.create({ }))
30+
const [error3,, isDefined3] = await safe(orpc.post.create({ }))
3131

3232
expect(isDefined3).toBe(false)
3333
expect(error3).toBeInstanceOf(ORPCError)

packages/server/src/procedure-client.test-d.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,10 +29,10 @@ describe('ProcedureClient', () => {
2929
})
3030

3131
it('works', async () => {
32-
const [output, error, isDefined] = await safe(client({ input: 123 }, { context: { cache: true } }))
32+
const [error, data, isDefined] = await safe(client({ input: 123 }, { context: { cache: true } }))
3333

3434
if (!error) {
35-
expectTypeOf(output).toEqualTypeOf<{ output: string }>()
35+
expectTypeOf(data).toEqualTypeOf<{ output: string }>()
3636
}
3737

3838
if (isDefined) {

packages/server/src/procedure-utils.test-d.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,10 @@ import { ping, pong } from '../tests/shared'
44
import { call } from './procedure-utils'
55

66
it('call', async () => {
7-
const [output, error, isDefined] = await safe(call(ping, { input: 123 }, { context: { db: 'postgres' } }))
7+
const [error, data, isDefined] = await safe(call(ping, { input: 123 }, { context: { db: 'postgres' } }))
88

99
if (!error) {
10-
expectTypeOf(output).toEqualTypeOf<{ output: string }>()
10+
expectTypeOf(data).toEqualTypeOf<{ output: string }>()
1111
}
1212

1313
if (isDefined) {

0 commit comments

Comments
 (0)