Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add castResult function #134

Closed
wants to merge 14 commits into from
15 changes: 15 additions & 0 deletions benchmarks/function/castResult.bench.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import * as _ from 'radashi'
import { bench } from 'vitest'

describe('castResult', () => {
const p = new Promise(() => {})
const res: _.Ok<1> = [undefined, 1]

bench('with promise input', () => {
_.castResult(p)
})

bench('with result input', () => {
_.castResult(res)
})
})
97 changes: 97 additions & 0 deletions docs/function/castResult.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
---
title: toResult
description: Converts a value or a promise into a Result tuple or ResultPromise.
tableOfContents:
maxHeadingLevel: 2
---

### Usage

The `toResult` function is designed to handle both synchronous and asynchronous values, converting them into a standardized `Result` format.

This function is particularly useful for ensuring consistency in handling both errors and successful outcomes, regardless of whether the underlying computation is synchronous or asynchronous.

## Call Signatures

#### Synchronous Values

The first argument is almost always the error. The one exception is passing a promise as the first argument (see “Asynchronous Values” below).

```typescript
import * as _ from 'radashi'

const result = _.toResult(null, 'hello')
// ^? Ok<'hello'>
// => [undefined, 'hello']

const result2 = _.toResult(new TypeError('oops'))
// ^? Err<TypeError>
// => [new TypeError('oops'), undefined]
```

#### Asynchronous Values

- If either argument is a promise, the result is a promise.

- **Typing:** The `ResultPromise<TResult, TError>` type is often used to represent the return type of these calls.

```typescript
import * as _ from 'radashi'

const result = _.toResult(null, Promise.resolve('hello'))
// ^? ResultPromise<'hello'>
// => Promise<[undefined, 'hello']>

const result2 = _.toResult(Promise.resolve('hello'))
// ^? ResultPromise<'hello'>
// => Promise<[undefined, 'hello']>
```

## Edge Cases

#### Handling Rejected Promises

```typescript
import * as _ from 'radashi'

const result = _.toResult(Promise.reject(new TypeError('oops')))
// ^? ResultPromise<unknown>
// => Promise<[new TypeError('oops'), undefined]>

const result2 = _.toResult(null, Promise.reject(new TypeError('oops')))
// ^? Promise<Err<Error>>
// => Promise<[new TypeError('oops'), undefined]>
```

#### Handling Result Objects

```typescript
import * as _ from 'radashi'

const result = _.toResult(null, Promise.resolve([undefined, 'hello']))
// ^? ResultPromise<'hello'>
// => Promise<[undefined, 'hello']>

const result2 = _.toResult(Promise.resolve([undefined, 'hello']))
// ^? ResultPromise<'hello'>
// => Promise<[undefined, 'hello']>
```

#### Mixed Types

```typescript
import * as _ from 'radashi'

const result = _.toResult(new TypeError('oops'), Promise.resolve('hello'))
// ^? Err<TypeError>
// => Promise<[new TypeError('oops'), undefined]>

const result2 = _.toResult(null, null)
// ^? Ok<null>
// => [undefined, null]

const rejectedPromise: Promise<unknown> = Promise.reject(new TypeError('oops'))
const result3 = _.toResult(rejectedPromise, 'hello')
// ^? ResultPromise<unknown>
// => Promise<[new TypeError('oops'), undefined]>
```
94 changes: 94 additions & 0 deletions docs/typed/isResult.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
---
title: isResult
description: Check if a value is a Result tuple
---

### Usage

Check if a value is a `Result` tuple.

**Don't know what that is?** Read the [Result](#result) section further down.

```ts
import * as _ from 'radashi'

_.isResult([undefined, 42]) // => true
_.isResult([new Error(), undefined]) // => true

// Result tuples cannot have both a value and an error, or neither.
_.isResult([undefined, undefined]) // => false
_.isResult([new Error(), true]) // => false

// Tuple must be of length 2.
_.isResult([new Error()]) // => false
_.isResult([undefined, true, undefined]) // => false

// Non-tuple values are false.
_.isResult([]) // => false
_.isResult({}) // => false
_.isResult(null) // => false
```

## Types In-Depth

### Result

“Results” are tuples of 2 elements (an **error** and a **result value**).

- The first element is always the **error**, or `undefined` if the operation was successful.
- The second element is always the **result value**, or `undefined` if the operation failed.
- These tuples are represented by the `Result<TResult, TError>` type.
- A default error type of `Error` is used when no error type is explicitly defined (e.g. `Result<string>`).
- The default error type is _not_ `unknown` because we assume you're following best practices and so you avoid throwing anything but `Error` objects.
- You're free to define the error type to be anything (like `Result<string, Error | number>`), not just `Error` types.

### Ok and Err

There are 2 types of result: `Ok<TResult>` and `Err<TError>`.

- The `Ok` type represents a successful operation. It's a tuple of `[undefined, TResult]`.
- The `Err` type represents a failed operation. It's a tuple of `[TError, undefined]`.

The names "Ok" and "Err" are inspired by Rust's `std::result` module.

To check for an `Ok` result, do this:

```ts
if (isResult(value) && value[0] == null) {
value // <-- now an Ok<unknown> type
value[1] // <-- This is the resulting value!
}
```

To check for an `Err` result, do this:

```ts
if (isResult(value) && value[0] != null) {
value // <-- now an Err<Error> type
value[0] // <-- This is the error!
}
```

## Type Guards

### isResultOk

Check if a value is both a `Result` tuple and an `Ok` result.

```ts
if (isResultOk(value)) {
value // <-- now an Ok<unknown> type
value[1] // <-- This is the resulting value!
}
```

### isResultErr

Check if a value is both a `Result` tuple and an `Err` result.

```ts
if (isResultErr(value)) {
value // <-- now an Err<Error> type
value[0] // <-- This is the error!
}
```
12 changes: 10 additions & 2 deletions src/async/defer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ export async function defer<TResponse>(
fn: (error?: any) => any
rethrow: boolean
}[] = []

const register = (
fn: (error?: any) => any,
options?: { rethrow?: boolean },
Expand All @@ -39,15 +40,22 @@ export async function defer<TResponse>(
fn,
rethrow: options?.rethrow ?? false,
})
const [err, response] = await tryit(func)(register)

const [err, response] = await tryit<
Parameters<typeof func>,
Promise<unknown>,
unknown
>(func)(register)

for (const { fn, rethrow } of callbacks) {
const [rethrown] = await tryit(fn)(err)
if (rethrown && rethrow) {
throw rethrown
}
}

if (err) {
throw err
}
return response
return response as TResponse
}
6 changes: 5 additions & 1 deletion src/async/parallel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,11 @@ export async function parallel<T, K>(
if (!next) {
return res(results)
}
const [error, result] = await tryit(func)(next.item)
const [error, result] = await tryit<
Parameters<typeof func>,
Promise<unknown>,
unknown
>(func)(next.item)
results.push({
error,
result: result as K,
Expand Down
42 changes: 8 additions & 34 deletions src/async/tryit.ts
Original file line number Diff line number Diff line change
@@ -1,47 +1,21 @@
import { isPromise } from 'radashi'

/**
* The result of a `tryit` function.
*
* If the function returns a promise, the result is a promise that
* resolves to an error-first callback-_like_ array response as
* `[Error, result]`.
*
* If the function returns a non-promise, the result is an error-first
* callback-_like_ array response as `[Error, result]`.
*
* @see https://radashi-org.github.io/reference/async/tryit
* @example
* ```ts
* const [err, result] = await tryit(async () => {
* return await fetch('https://example.com')
* })
* ```
*/
export type TryitResult<Return> = Return extends Promise<any>
? Promise<[Error, undefined] | [undefined, Awaited<Return>]>
: [Error, undefined] | [undefined, Return]
import { castResult, type CastResult } from 'radashi'

/**
* A helper to try an async function without forking the control flow.
* Returns an error-first callback-_like_ array response as `[Error,
* result]`
*/
export function tryit<Args extends any[], Return>(
func: (...args: Args) => Return,
): (...args: Args) => TryitResult<Return> {
export function tryit<TArgs extends any[], TReturn, TError = Error>(
func: (...args: TArgs) => TReturn,
): (...args: TArgs) => CastResult<TReturn, TError> {
return (...args) => {
let result: TReturn
try {
const result = func(...args)
if (isPromise(result)) {
return result
.then(value => [undefined, value])
.catch(err => [err, undefined]) as TryitResult<Return>
}
return [undefined, result] as TryitResult<Return>
result = func(...args)
} catch (err) {
return [err, undefined] as TryitResult<Return>
return [err, undefined] as CastResult<TReturn, TError>
}
return castResult(null, result)
}
}

Expand Down
Loading
Loading