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 unwrapOrElse #593

Closed
wants to merge 3 commits into from
Closed
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
5 changes: 5 additions & 0 deletions .changeset/unwrapOrElse.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'neverthrow': minor
---

Add unwrapOrElse
52 changes: 52 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ For asynchronous tasks, `neverthrow` offers a `ResultAsync` class which wraps a
- [`Result.map` (method)](#resultmap-method)
- [`Result.mapErr` (method)](#resultmaperr-method)
- [`Result.unwrapOr` (method)](#resultunwrapor-method)
- [`Result.unwrapOrElse` (method)](#resultunwraporelse-method)
- [`Result.andThen` (method)](#resultandthen-method)
- [`Result.asyncAndThen` (method)](#resultasyncandthen-method)
- [`Result.orElse` (method)](#resultorelse-method)
Expand All @@ -51,6 +52,7 @@ For asynchronous tasks, `neverthrow` offers a `ResultAsync` class which wraps a
- [`ResultAsync.map` (method)](#resultasyncmap-method)
- [`ResultAsync.mapErr` (method)](#resultasyncmaperr-method)
- [`ResultAsync.unwrapOr` (method)](#resultasyncunwrapor-method)
- [`ResultAsync.unwrapOrElse` (method)](#resultasyncunwraporelse-method)
- [`ResultAsync.andThen` (method)](#resultasyncandthen-method)
- [`ResultAsync.orElse` (method)](#resultasyncorelse-method)
- [`ResultAsync.match` (method)](#resultasyncmatch-method)
Expand Down Expand Up @@ -318,6 +320,32 @@ const unwrapped: number = myResult.map(multiply).unwrapOr(10)

---

#### `Result.unwrapOrElse` (method)

Unwrap the `Ok` value, or return the default by function if there is an `Err`

**Signature:**

```typescript
class Result<T, E> {
unwrapOrElse<A>(err: (e: E) => A): T | A { ... }
}
```

**Example**:

```typescript
const myResult = err('Oh noooo')

const multiply = (value: number): number => value * 2

const unwrapped: number = myResult.map(multiply).unwrapOrElse(() => 10)
```

[⬆️ Back to top](#toc)

---

#### `Result.andThen` (method)

Same idea as `map` above. Except you must return a new `Result`.
Expand Down Expand Up @@ -1114,6 +1142,30 @@ const unwrapped: number = await errAsync(0).unwrapOr(10)

---

#### `ResultAsync.unwrapOrElse` (method)

Unwrap the `Ok` value, or return the default by function if there is an `Err`.
Works just like `Result.unwrapOrElse` but returns a `Promise<T>` instead of `T`.

**Signature:**

```typescript
class ResultAsync<T, E> {
unwrapOrElse<A>(err: (e: E) => A): Promise<T | A> { ... }
}
```

**Example**:

```typescript
const unwrapped: number = await errAsync(0).unwrapOrElse(() => 10)
// unwrapped = 10
```

[⬆️ Back to top](#toc)

---

#### `ResultAsync.andThen` (method)

Same idea as `map` above. Except the applied function must return a `Result` or `ResultAsync`.
Expand Down
4 changes: 4 additions & 0 deletions src/result-async.ts
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,10 @@ export class ResultAsync<T, E> implements PromiseLike<Result<T, E>> {
return this._promise.then((res) => res.unwrapOr(t))
}

unwrapOrElse<A>(err: (e: E) => A): Promise<T | A> {
return this._promise.then((res) => res.unwrapOrElse(err))
}

/**
* Emulates Rust's `?` operator in `safeTry`'s body. See also `safeTry`.
*/
Expand Down
16 changes: 16 additions & 0 deletions src/result.ts
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,13 @@ interface IResult<T, E> {
*/
unwrapOr<A>(v: A): T | A

/**
* Unwrap the `Ok` value, or return the default by function if there is an `Err`
*
* @param err the function that return if there is an `Err`
*/
unwrapOrElse<A>(err: (e: E) => A): T | A

/**
*
* Given 2 functions (one for the `Ok` variant and one for the `Err` variant)
Expand Down Expand Up @@ -361,6 +368,11 @@ export class Ok<T, E> implements IResult<T, E> {
return this.value
}

// eslint-disable-next-line @typescript-eslint/no-unused-vars
unwrapOrElse<A>(_err: (e: E) => A): T | A {
return this.value
}

// eslint-disable-next-line @typescript-eslint/no-unused-vars
match<A, B = A>(ok: (t: T) => A, _err: (e: E) => B): A | B {
return ok(this.value)
Expand Down Expand Up @@ -447,6 +459,10 @@ export class Err<T, E> implements IResult<T, E> {
return v
}

unwrapOrElse<A>(err: (e: E) => A): T | A {
return err(this.error)
}
Comment on lines +462 to +464
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I still find the naming err to be a bit confusing, but now I see that it's used everywhere for this type of argument 🤷🏼 e.g. in match directly below. no reason to break that pattern...


match<A, B = A>(_ok: (t: T) => A, err: (e: E) => B): A | B {
return err(this.error)
}
Expand Down
22 changes: 22 additions & 0 deletions tests/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,11 @@ describe('Result.Ok', () => {
expect(okVal.unwrapOr(1)).toEqual(12)
})

it('unwrapOrElse and return the Ok value', () => {
const okVal = ok(12)
expect(okVal.unwrapOrElse(() => 1)).toEqual(12)
})
Comment on lines +207 to +210
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's part of unwrapOrElse's contract to not call the callback for an Ok value.

It could make sense to test that (using jest.fn and expect(...).not.toHaveBeenCalled())


it('Maps to a ResultAsync', async () => {
const okVal = ok(12)

Expand Down Expand Up @@ -320,6 +325,11 @@ describe('Result.Err', () => {
expect(okVal.unwrapOr(1)).toEqual(1)
})

it('unwrapOrElse and return the default value', () => {
const okVal = err<number, string>('Oh nooo')
expect(okVal.unwrapOrElse(() => 1)).toEqual(1)
})

it('Skips over andThen', () => {
const errVal = err('Yolo')

Expand Down Expand Up @@ -1134,6 +1144,18 @@ describe('ResultAsync', () => {
})
})

describe('unwrapOrElse', () => {
it('returns a promise to the result value on an Ok', async () => {
const unwrapped = await okAsync(12).unwrapOrElse(() => 10)
expect(unwrapped).toBe(12)
})

it('returns a promise to the provided default function value on an Error', async () => {
const unwrapped = await errAsync<number, number>(12).unwrapOrElse(() => 10)
expect(unwrapped).toBe(10)
})
})

describe('fromSafePromise', () => {
it('Creates a ResultAsync from a Promise', async () => {
const res = ResultAsync.fromSafePromise(Promise.resolve(12))
Expand Down