Skip to content

Commit

Permalink
feat!: Result type, wrapping Ok and Err values
Browse files Browse the repository at this point in the history
  • Loading branch information
johannschopplich committed Aug 20, 2024
1 parent 376b9a0 commit 5e03c8f
Show file tree
Hide file tree
Showing 6 changed files with 1,431 additions and 1,690 deletions.
239 changes: 143 additions & 96 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,65 +1,31 @@
# resultx

resultx is a streamlined utility library that simplifies error handling by wrapping promises or functions and returning an object or tuple with `data` and `error` properties. This eliminates the necessity of using try/catch blocks, enhancing the readability and maintainability of your code:
A lightweight and simple `Result` type for TypeScript, inspired by Rust's Result type.

<table>
## Description

<tr>
<th><p><strong>😮‍💨 Before</strong></p></th>
<th><p><strong>🙆‍♂️ After</strong></p></th>
</tr>
`resultx` provides a `Result` type that represents either success (`Ok`) or failure (`Err`). It helps to handle errors in a more explicit and type-safe way, without relying on exceptions.

<tr>
<td>
For error handling in synchronous code, `resultx` provides a `trySafe` function that wraps a function that might throw an error. For asynchronous code, `trySafe` can also be used with promises.

```ts
let result

try {
result = await client.getItems()
}
catch (error) {
console.error(error)
}
```

</td>
<td>

```ts
import { guardedInvoke } from 'resultx'

const { data, error } = await guardedInvoke(client.getItems())

if (error) {
console.error(error)
}
```

</td>
</tr>

</table>

If you prefer to use tuples instead of objects, you can also destructure the return value of `guardedInvoke` as a tuple:

```ts
import { guardedInvoke } from 'resultx'
## Key Features

// Destructuring a tuple is also supported
const [data, error] = await guardedInvoke(client.getItems())
```
- 🎭 Simple and intuitive `Result` type, wrapping `Ok` and `Err` values
- 🚀 Supports both synchronous and asynchronous operations
- 🛡️ Type-safe error handling
- 🧰 Zero dependencies
- 📦 Tiny bundle size (half a kilobyte minified)

## Key Features
## Table of Contents

- 💆‍♂️ Returns an object or tuple with `data` and `error` properties
- 📼 Functions can be synchronous or asynchronous
- 🛠️ Supports custom rejected Promise error types
- 🦾 Strongly typed
- [Installation](#installation)
- [Usage](#usage)
- [API](#api)
- [Examples](#examples)

## Installation

Installing resultx is as simple as running the following command:
Installing `resultx` is as simple as running one of the following commands, depending on your package manager:

```bash
pnpm add -D resultx
Expand All @@ -73,92 +39,173 @@ yarn add -D resultx

## Usage

Once installed, you can import the `guardedInvoke` function from resultx and use it to wrap promises or functions in your code. Below are some usage examples that illustrate the key functionalities of resultx:
```ts
import { err, ok, trySafe } from 'resultx'

// Create `Ok` and `Err` results
const successResult = ok(42)
// ^? Ok<number>
const failureResult = err('Something went wrong')
// ^? Err<"Something went wrong">

// Use `trySafe` for error handling
const result = trySafe(() => {
// Your code that might throw an error
return JSON.parse('{"key": "value"}')
})

### Handling Errors in Async Functions
if (result.ok) {
console.log('Parsed JSON:', result.value)
}
else {
console.error('Failed to parse JSON:', result.error)
}
```

resultx simplifies error handling in asynchronous functions using the `guardedInvoke` function. If an error occurs, it is assigned to the `error` property and can be handled accordingly.
## API

```ts
import { guardedInvoke } from 'resultx'
### `Result`

const { data, error } = await guardedInvoke(client.getItems())
The `Result` type represents either success (`Ok`) or failure (`Err`).

if (error) {
console.error(error)
}
**Type Definition:**

```ts
type Result<T, E> = Ok<T> | Err<E>
```
### Handling Errors in Synchronous Functions
#### `Ok`
The `Ok` type wraps a successful value.
The `guardedInvoke` function can also be used with synchronous functions.
**Type Definition:**
```ts
import { guardedInvoke } from 'resultx'
interface Ok<T> {
readonly ok: true
readonly value: T
}
```

const { data, error } = guardedInvoke(() => {
throw new Error('Something went wrong')
})
### `Err`

The `Err` type wraps an error value.

**Type Definition:**

if (error) {
console.error(error)
```ts
interface Err<E> {
readonly ok: false
readonly error: E
}
```

### Using Tuples
### `ok`

Shorthand function to create an `Ok` result. Use it to wrap a successful value.

In addition to returning objects, `guardedInvoke` can also return a tuple containing `data` and `error` values. This provides a neat, organized structure for error management:
**Type Definition:**

```ts
import { guardedInvoke } from 'resultx'
function ok<T>(value: T): Ok<T>
```

const [data, error] = await guardedInvoke(client.getItems())
### `err`

if (error) {
console.error(error)
}
Shorthand function to create an `Err` result. Use it to wrap an error value.

**Type Definition:**

```ts
function err<E extends string = string>(err: E): Err<E>
function err<E = unknown>(err: E): Err<E>
```

### Guarded Functions
### `trySafe`

Wraps a function that might throw an error and returns a `Result` with the result of the function.

With `guardedInvokeFn` you can create a function that can be called with the same arguments, but guarded. This is useful when you want to guard a function that you don't own, like `JSON.parse`:
**Type Definition:**

```ts
import { guardedInvokeFn } from 'resultx'
function trySafe<T, E = unknown>(fn: () => T): Result<T, E>
function trySafe<T, E = unknown>(promise: Promise<T>): Promise<Result<T, E>>
```

### `unwrap`

const safeJSONParse = guardedInvokeFn(JSON.parse)
Unwraps a `Result` and returns a tuple with the value and error: `{ value, error }`.

let result = safeJSONParse('{ "test": 1 }')
console.log(result.data) // { test: 1 }
**Type Definition:**

result = safeJSONParse('{ missing the other one')
console.log(result.error) // SyntaxError: Unexpected character 'm'
```ts
function unwrap<T>(result: Ok<T>): { value: T, error: undefined }
function unwrap<E>(result: Err<E>): { value: undefined, error: E }
```

## Custom Error Handling
## Examples

### Basic Usage

resultx offers the flexibility to implement custom error handling strategies by overriding the default error type. This can be done by passing a custom error type as the second argument to the `guardedInvoke` function:
A common use case for `Result` is error handling in functions that might fail. Here's an example of a function that divides two numbers and returns a `Result`:

```ts
import { guardedInvoke } from 'resultx'
import { err, ok } from 'resultx'
class CustomError extends Error {}
function divide(a: number, b: number) {
if (b === 0) {
return err('Division by zero')
}
return ok(a / b)
}
const result = divide(10, 2)
if (result.ok) {
console.log('Result:', result.value)
}
else {
console.error('Error:', result.error)
}
```

### Error Handling with `trySafe`

const [data, error] = guardedInvoke(() => {
throw new CustomError('Something went wrong')
}, CustomError)
The `trySafe` function is useful for error handling in synchronous code. It wraps a function that might throw an error and returns a `Result`:

```ts
import { trySafe } from 'resultx'
// The `error` variable will properly be typed as `CustomError`
if (error) {
console.log(error instanceof CustomError) // `true`
console.error(error)
const jsonResult = trySafe(() => JSON.parse('{"key": "value"}'))
if (jsonResult.ok) {
console.log('Parsed JSON:', jsonResult.value)
}
else {
console.error('Failed to parse JSON:', jsonResult.error)
}
```

## Credits
### Async Operations with `trySafe`

- [Henrique Cunha](https://github.com/henrycunh) for his [resguard](https://github.com/henrycunh/resguard) library, which inspired this project.
- [Anthony Fu](https://github.com/antfu) for his post on [destructuring with object or array](https://antfu.me/posts/destructuring-with-object-or-array).
For asynchronous operations, `trySafe` can also be used with promises. Here's an example of fetching data from an API:

```ts
import { trySafe } from 'resultx'
async function fetchData() {
const result = await trySafe(fetch('https://api.example.com/data'))
if (result.ok) {
const data = await result.value.json()
console.log('Fetched data:', data)
}
else {
console.error('Failed to fetch data:', result.error)
}
}
fetchData()
```

## License

Expand Down
19 changes: 10 additions & 9 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@
"name": "resultx",
"type": "module",
"version": "1.0.2",
"packageManager": "pnpm@9.4.0",
"description": "Streamlined, type-safe error handling",
"packageManager": "pnpm@9.7.1",
"description": "Minimalist, strongly-typed result pattern for TypeScript",
"author": "Johann Schopplich <hello@johannschopplich.com>",
"license": "MIT",
"homepage": "https://github.com/johannschopplich/resultx#readme",
Expand All @@ -14,6 +14,7 @@
"bugs": "https://github.com/johannschopplich/resultx/issues",
"keywords": [
"result",
"result-type",
"rust",
"try-catch"
],
Expand All @@ -38,12 +39,12 @@
"release": "bumpp"
},
"devDependencies": {
"@antfu/eslint-config": "^2.21.3",
"@types/node": "^20.14.10",
"bumpp": "^9.4.1",
"eslint": "^9.6.0",
"typescript": "^5.5.3",
"unbuild": "^3.0.0-rc.6",
"vitest": "^1.6.0"
"@antfu/eslint-config": "^2.26.0",
"@types/node": "^20.16.1",
"bumpp": "^9.5.1",
"eslint": "^9.9.0",
"typescript": "^5.5.4",
"unbuild": "^3.0.0-rc.7",
"vitest": "^2.0.5"
}
}
Loading

0 comments on commit 5e03c8f

Please sign in to comment.