Skip to content

Commit

Permalink
Add AsyncOption (#114)
Browse files Browse the repository at this point in the history
Something that comes up every now and then and I've been missing it from
the ts-results-es API: an asynchronous counterpart to Option.

Also a counterpart to AsyncResult added in [1].

This addition completes the sync/async, Option/Result matrix.

[1] 3f55d15 ("Take a first stab at async results (#87)")
  • Loading branch information
jstasiak authored Mar 4, 2024
1 parent 51ac236 commit f936cab
Show file tree
Hide file tree
Showing 11 changed files with 409 additions and 0 deletions.
150 changes: 150 additions & 0 deletions docs/reference/api/asyncoption.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
AsyncOption
===========

An async-aware :doc:`option` counterpart.

Can be combined with asynchronous code without having to ``await`` anything right until
the moment when you're ready to extract the final ``Option`` out of it.

Can also be combined with synchronous code for convenience.

.. code-block:: typescript
// T is the value type
AsyncOption<T>
Imports:

.. code-block:: typescript
import { AsyncOption } from 'ts-results-es'
Construction:

You can construct it directly from ``Option<T>`` or ``Promise<Option<T>>``:

.. code-block:: typescript
const option1 = new AsyncOption(Some(1))
const option2 = new AsyncOption((async () => None)())
Or you can use the :ref:`Option.toAsyncOption() <toAsyncOption>` method:

.. code-block:: typescript
const option3 = Some(1).toAsyncOption()
``andThen()``
-------------

.. code-block:: typescript
andThen<T2>(
mapper: (val: T) => Option<T2> | Promise<Option<T2>> | AsyncOption<T2>,
): AsyncOption<T2>
Calls ``mapper`` if the option is ``Some``, otherwise keeps the ``None`` value intact.
This function can be used for control flow based on ``Option`` values.

Example:

.. code-block:: typescript
let hasValue = Some(1).toAsyncOption()
let noValue = None.toAsyncOption()
await hasValue.andThen(async (value) => Some(value * 2)).promise // Some(2)
await hasValue.andThen(async (value) => None).promise // None
await noValue.andThen(async (value) => Ok(value * 2)).promise // None
``map()``
---------

.. code-block:: typescript
map<U>(mapper: (val: T) => U | Promise<U>): AsyncOption<U>
Maps an ``AsyncOption<T>`` to ``AsyncOption<U>`` by applying a function to a contained
``Some`` value, leaving a ``None`` value untouched.

This function can be used to compose the results of two functions.

Example:

.. code-block:: typescript
let hasValue = Ok(1).toAsyncOption()
let noValue = None.toAsyncOption()
await hasValue.map(async (value) => value * 2).promise // Some(2)
await noValue.map(async (value) => value * 2).promise // None
``or()``
--------

.. code-block:: typescript
or<U>(other: Option<U> | AsyncOption<U> | Promise<Option<U>>): AsyncOption<T | U>
Returns the value from ``other`` if this ``AsyncOption`` contains ``None``, otherwise returns self.

If ``other`` is a result of a function call consider using :ref:`AsyncOption.orElse` instead, it will
only evaluate the function when needed.

Example:

.. code-block:: typescript
const noValue = new AsyncOption(None)
const hasValue = new AsyncOption(Some(1))
await noValue.or(Some(123)).promise // Some(123)
await hasValue.or(Some(123)).promise // Some(1)
.. _AsyncOption.orElse:

``orElse()``
------------

.. code-block:: typescript
orElse<U>(other: () => Option<U> | AsyncOption<U> | Promise<Option<U>>): AsyncOption<T | U>
Returns the value obtained by calling ``other`` if this ``AsyncOption`` contains ``None``, otherwise
returns self.

Example:

.. code-block:: typescript
const noValue = new AsyncOption(None)
const hasValue = new AsyncOption(Some(1))
await noValue.orElse(() => Some(123)).promise // Some(123)
await hasValue.orElse(() => Some(123)).promise // Some(1)
``promise``
-----------

.. code-block:: typescript
promise: Promise<Result<T, E>>
A promise that resolves to a synchronous result.

Await it to convert ``AsyncResult<T, E>`` to ``Result<T, E>``.


``toResult()``
--------------

.. code-block:: typescript
toResult<E>(error: E): AsyncResult<T, E>
Converts an ``AsyncOption<T>`` to an ``AsyncResult<T, E>`` so that ``None`` is converted to
``Err(error)`` and ``Some`` is converted to ``Ok``.
11 changes: 11 additions & 0 deletions docs/reference/api/asyncresult.rst
Original file line number Diff line number Diff line change
Expand Up @@ -161,3 +161,14 @@ Example:
A promise that resolves to a synchronous result.
Await it to convert ``AsyncResult<T, E>`` to ``Result<T, E>``.
``toOption()``
--------------
.. code-block:: typescript
toOption(): AsyncOption<T>
Converts from ``AsyncResult<T, E>`` to ``AsyncOption<T>`` so that ``Err`` is converted to ``None``
and ``Ok`` is converted to ``Some``.
1 change: 1 addition & 0 deletions docs/reference/api/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ API reference

option
result
asyncoption
asyncresult
rxjs

Expand Down
14 changes: 14 additions & 0 deletions docs/reference/api/option.rst
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,20 @@ Example:
Some(1).orElse(() => Some(2)) // => Some(1)
None.orElse(() => Some(2)) // => Some(2)
.. _toAsyncOption:

``toAsyncOption()``
-------------------

.. code-block:: typescript
toAsyncOption(): AsyncOption<T>
Creates an `AsyncOption` based on this `Option`.

Useful when you need to compose results with asynchronous code.


``toResult()``
--------------

Expand Down
133 changes: 133 additions & 0 deletions src/asyncoption.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
import { AsyncResult } from './asyncresult.js'
import { Option, Some } from './option.js'

/**
* An async-aware `Option` counterpart.
*
* Can be combined with asynchronous code without having to ``await`` anything right until
* the moment when you're ready to extract the final ``Option`` out of it.
*
* Can also be combined with synchronous code for convenience.
*/
export class AsyncOption<T> {
/**
* A promise that resolves to a synchronous ``Option``.
*
* Await it to convert `AsyncOption<T>` to `Option<T>`.
*/
promise: Promise<Option<T>>

/**
* Constructs an `AsyncOption` from an `Option` or a `Promise` of an `Option`.
*
* @example
* ```typescript
* const option = new AsyncOption(Promise.resolve(Some('username')))
* ```
*/
constructor(start: Option<T> | Promise<Option<T>>) {
this.promise = Promise.resolve(start)
}

/**
* Calls `mapper` if the option is `Some`, otherwise keeps the `None` value intact.
* This function can be used for control flow based on `Option` values.
*
* @example
* ```typescript
* let hasValue = Some(1).toAsyncOption()
* let noValue = None.toAsyncOption()
*
* await hasValue.andThen(async (value) => Some(value * 2)).promise // Some(2)
* await hasValue.andThen(async (value) => None).promise // None
* await noValue.andThen(async (value) => Ok(value * 2)).promise // None
* ```
*/
andThen<T2>(mapper: (val: T) => Option<T2> | Promise<Option<T2>> | AsyncOption<T2>): AsyncOption<T2> {
return this.thenInternal(async (option) => {
if (option.isNone()) {
return option
}
const mapped = mapper(option.value)
return mapped instanceof AsyncOption ? mapped.promise : mapped
})
}

/**
* Maps an `AsyncOption<T>` to `AsyncOption<U>` by applying a function to a contained
* `Some` value, leaving a `None` value untouched.
*
* This function can be used to compose the results of two functions.
*
* @example
* ```typescript
* let hasValue = Ok(1).toAsyncOption()
* let noValue = None.toAsyncOption()
*
* await hasValue.map(async (value) => value * 2).promise // Some(2)
* await noValue.map(async (value) => value * 2).promise // None
* ```
*/
map<U>(mapper: (val: T) => U | Promise<U>): AsyncOption<U> {
return this.thenInternal(async (option) => {
if (option.isNone()) {
return option
}
return Some(await mapper(option.value))
})
}

/**
* Returns the value from `other` if this `AsyncOption` contains `None`, otherwise returns self.
*
* If `other` is a result of a function call consider using `orElse` instead, it will
* only evaluate the function when needed.
*
* @example
* ```
* const noValue = new AsyncOption(None)
* const hasValue = new AsyncOption(Some(1))
*
* await noValue.or(Some(123)).promise // Some(123)
* await hasValue.or(Some(123)).promise // Some(1)
* ```
*/
or<U>(other: Option<U> | AsyncOption<U> | Promise<Option<U>>): AsyncOption<T | U> {
return this.orElse(() => other)
}

/**
* Returns the value obtained by calling `other` if this `AsyncOption` contains `None`, otherwise
* returns self.
*
* @example
* ```
* const noValue = new AsyncOption(None)
* const hasValue = new AsyncOption(Some(1))
*
* await noValue.orElse(() => Some(123)).promise // Some(123)
* await hasValue.orElse(() => Some(123)).promise // Some(1)
* ```
*/
orElse<U>(other: () => Option<U> | AsyncOption<U> | Promise<Option<U>>): AsyncOption<T | U> {
return this.thenInternal(async (option): Promise<Option<T | U>> => {
if (option.isSome()) {
return option
}
const otherValue = other()
return otherValue instanceof AsyncOption ? otherValue.promise : otherValue
})
}

/**
* Converts an `AsyncOption<T>` to an `AsyncResult<T, E>` so that `None` is converted to
* `Err(error)` and `Some` is converted to `Ok`.
*/
toResult<E>(error: E): AsyncResult<T, E> {
return new AsyncResult(this.promise.then(option => option.toResult(error)))
}

private thenInternal<T2>(mapper: (option: Option<T>) => Promise<Option<T2>>): AsyncOption<T2> {
return new AsyncOption(this.promise.then(mapper))
}
}
9 changes: 9 additions & 0 deletions src/asyncresult.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { AsyncOption } from './asyncoption.js'
import { Err, Result, Ok } from './result.js'

/**
Expand Down Expand Up @@ -144,6 +145,14 @@ export class AsyncResult<T, E> {
})
}

/**
* Converts from `AsyncResult<T, E>` to `AsyncOption<T>` so that `Err` is converted to `None`
* and `Ok` is converted to `Some`.
*/
toOption(): AsyncOption<T> {
return new AsyncOption(this.promise.then(result => result.toOption()))
}

private thenInternal<T2, E2>(mapper: (result: Result<T, E>) => Promise<Result<T2, E2>>): AsyncResult<T2, E2> {
return new AsyncResult(this.promise.then(mapper))
}
Expand Down
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from './asyncoption.js';
export * from './asyncresult.js';
export * from './result.js';
export * from './option.js';
16 changes: 16 additions & 0 deletions src/option.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { AsyncOption } from './asyncoption.js'
import { toString } from './utils.js';
import { Result, Ok, Err } from './result.js';

Expand Down Expand Up @@ -100,6 +101,13 @@ interface BaseOption<T> extends Iterable<T extends Iterable<infer U> ? U : never
* Maps an `Option<T>` to a `Result<T, E>`.
*/
toResult<E>(error: E): Result<T, E>;

/**
* Creates an `AsyncOption` based on this `Option`.
*
* Useful when you need to compose results with asynchronous code.
*/
toAsyncOption(): AsyncOption<T>
}

/**
Expand Down Expand Up @@ -165,6 +173,10 @@ class NoneImpl implements BaseOption<never> {
toString(): string {
return 'None';
}

toAsyncOption(): AsyncOption<never> {
return new AsyncOption<never>(None)
}
}

// Export None as a singleton, then freeze it so it can't be modified
Expand Down Expand Up @@ -251,6 +263,10 @@ class SomeImpl<T> implements BaseOption<T> {
return Ok(this.value);
}

toAsyncOption(): AsyncOption<T> {
return new AsyncOption(this)
}

/**
* Returns the contained `Some` value, but never throws.
* Unlike `unwrap()`, this method doesn't throw and is only callable on an Some<T>
Expand Down
Loading

0 comments on commit f936cab

Please sign in to comment.