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

Implement AsyncOption #114

Merged
merged 1 commit into from
Mar 4, 2024
Merged
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
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 @@ -159,6 +160,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
Loading