true-myth
Version:
A library for safe functional programming in JavaScript, with first-class support for TypeScript
1,358 lines (1,105 loc) • 78.6 kB
TypeScript
/**
A {@linkcode Task Task<T, E>} is a type representing an asynchronous operation
that may fail, with a successful (“resolved”) value of type `T` and an error
(“rejected”) value of type `E`.
If the `Task` is pending, it is {@linkcode Pending}. If it has resolved, it is
{@linkcode Resolved Resolved(value)}. If it has rejected, it is {@linkcode
Rejected Rejected(reason)}.
For more, see [the guide](/guide/understanding/task/).
@module
*/
import Maybe from './maybe.js';
import Result from './result.js';
import type { AnyResult, SomeResult } from './result.js';
import Unit from './unit.js';
import * as delay from './task/delay.js';
export {
/**
Re-exports `true-myth/task/delay` as a namespace object for convenience.
```ts
import * as task from 'true-myth/task';
let strategy = task.delay.exponential({ from: 5, withFactor: 5 }).take(5);
```
*/
delay,
/**
Re-exports `true-myth/task/delay` as a namespace object.
@deprecated Use `delay` instead:
```ts
import * as task from 'true-myth/task';
let strategy = task.delay.exponential({ from: 5, withFactor: 5 }).take(5);
```
The `Delay` namespace re-export will be removed in favor of the `delay`
re-export in v10.
*/
delay as Delay, };
declare const IsTask: unique symbol;
type SomeTask<T, E> = {
[IsTask]: [T, E];
};
/** @internal */
type TypesFor<S extends AnyTask | AnyResult> = S extends SomeTask<infer T, infer E> ? {
resolution: T;
rejection: E;
} : S extends SomeResult<infer T, infer E> ? {
resolution: T;
rejection: E;
} : never;
/**
Internal implementation details for {@linkcode Task}.
*/
declare class TaskImpl<T, E> implements PromiseLike<Result<T, E>> {
#private;
/** @internal */
readonly [IsTask]: [T, E];
/**
Construct a new `Task`, using callbacks to wrap APIs which do not natively
provide a `Promise`.
This is identical to the [Promise][promise] constructor, with one very
important difference: rather than producing a value upon resolution and
throwing an exception when a rejection occurs like `Promise`, a `Task`
always “succeeds” in producing a usable value, just like {@linkcode Result}
for synchronous code.
[promise]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/Promise
For constructing a `Task` from an existing `Promise`, see:
- {@linkcode fromPromise}
- {@linkcode safelyTry}
- {@linkcode tryOr}
- {@linkcode tryOrElse}
For constructing a `Task` immediately resolved or rejected with given
values, see {@linkcode Task.resolve} and {@linkcode Task.reject}
respectively.
@param executor A function which the constructor will execute to manage
the lifecycle of the `Task`. The executor in turn has two functions as
parameters: one to call on resolution, the other on rejection.
*/
constructor(executor: (resolve: (value: T) => void, reject: (reason: E) => void) => void);
then<A, B>(onSuccess?: ((result: Result<T, E>) => A | PromiseLike<A>) | null | undefined, onRejected?: ((reason: unknown) => B | PromiseLike<B>) | null | undefined): PromiseLike<A | B>;
toString(): string;
/**
Construct a `Task` which is already resolved. Useful when you have a value
already, but need it to be available in an API which expects a `Task`.
@group Constructors
*/
static resolve<T extends Unit, E = never>(): Task<Unit, E>;
/**
Construct a `Task` which is already resolved. Useful when you have a value
already, but need it to be available in an API which expects a `Task`.
@group Constructors
*/
static resolve<T, E = never>(value: T): Task<T, E>;
/**
Construct a `Task` which is already rejected. Useful when you have an error
already, but need it to be available in an API which expects a `Task`.
@group Constructors
*/
static reject<T = never, E extends {} = {}>(): Task<T, Unit>;
/**
Construct a `Task` which is already rejected. Useful when you have an error
already, but need it to be available in an API which expects a `Task`.
@group Constructors
*/
static reject<T = never, E = unknown>(reason: E): Task<T, E>;
/**
Create a pending `Task` and supply `resolveWith` and `rejectWith` helpers,
similar to the [`Promise.withResolvers`][pwr] static method, but producing a
`Task` with the usual safety guarantees.
[pwr]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/withResolvers
## Examples
### Resolution
```ts
let { task, resolveWith, rejectWith } = Task.withResolvers<string, Error>();
resolveWith("Hello!");
let result = await task.map((s) => s.length);
let length = result.unwrapOr(0);
console.log(length); // 5
```
### Rejection
```ts
let { task, resolveWith, rejectWith } = Task.withResolvers<string, Error>();
rejectWith(new Error("oh teh noes!"));
let result = await task.mapRejection((s) => s.length);
let errLength = result.isErr ? result.error : 0;
console.log(errLength); // 5
```
@group Constructors
*/
static withResolvers<T, E>(): WithResolvers<T, E>;
get state(): State;
get isPending(): boolean;
get isResolved(): boolean;
get isRejected(): boolean;
/**
The value of a resolved `Task`.
> [!WARNING]
> It is an error to access this property on a `Task` which is `Pending` or
> `Rejected`.
*/
get value(): T;
/**
The cause of a rejection.
> [!WARNING]
> It is an error to access this property on a `Task` which is `Pending` or
> `Resolved`.
*/
get reason(): E;
/**
Map over a {@linkcode Task} instance: apply the function to the resolved
value if the task completes successfully, producing a new `Task` with the
value returned from the function. If the task failed, return the rejection
as {@linkcode Rejected} without modification.
`map` works a lot like [`Array.prototype.map`][array-map], but with one
important difference. Both `Task` and `Array` are kind of like a “container”
for other kinds of items, but where `Array.prototype.map` has 0 to _n_
items, a `Task` represents the possibility of an item being available at
some point in the future, and when it is present, it is *either* a success
or an error.
[array-map]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/map
Where `Array.prototype.map` will apply the mapping function to every item in
the array (if there are any), `Task.map` will only apply the mapping
function to the resolved element if it is `Resolved`.
If you have no items in an array of numbers named `foo` and call `foo.map(x
=> x + 1)`, you'll still some have an array with nothing in it. But if you
have any items in the array (`[2, 3]`), and you call `foo.map(x => x + 1)`
on it, you'll get a new array with each of those items inside the array
"container" transformed (`[3, 4]`).
With this `map`, the `Rejected` variant is treated *by the `map` function*
kind of the same way as the empty array case: it's just ignored, and you get
back a new `Task` that is still just the same `Rejected` instance. But if
you have an `Resolved` variant, the map function is applied to it, and you
get back a new `Task` with the value transformed, and still `Resolved`.
## Examples
```ts
import Task from 'true-myth/task';
const double = n => n * 2;
const aResolvedTask = Task.resolve(12);
const mappedResolved = aResolvedTask.map(double);
let resolvedResult = await aResolvedTask;
console.log(resolvedResult.toString()); // Ok(24)
const aRejectedTask = Task.reject("nothing here!");
const mappedRejected = aRejectedTask.map(double);
let rejectedResult = await aRejectedTask;
console.log(rejectedResult.toString()); // Err("nothing here!")
```
@template T The type of the resolved value.
@template U The type of the resolved value of the returned `Task`.
@param mapFn The function to apply the value to when the `Task` finishes if
it is `Resolved`.
*/
map<U>(mapFn: (t: T) => U): Task<U, E>;
/**
Map over a {@linkcode Task}, exactly as in {@linkcode map}, but operating on
the rejection reason if the `Task` rejects, producing a new `Task`, still
rejected, with the value returned from the function. If the task completed
successfully, return it as `Resolved` without modification. This is handy
for when you need to line up a bunch of different types of errors, or if you
need an error of one shape to be in a different shape to use somewhere else
in your codebase.
## Examples
```ts
import Task from 'true-myth/task';
const extractReason = (err: { code: number, reason: string }) => err.reason;
const aResolvedTask = Task.resolve(12);
const mappedResolved = aResolvedTask.mapRejected(extractReason);
console.log(mappedOk)); // Ok(12)
const aRejectedTask = Task.reject({ code: 101, reason: 'bad file' });
const mappedRejection = await aRejectedTask.mapRejected(extractReason);
console.log(toString(mappedRejection)); // Err("bad file")
```
@template T The type of the value produced if the `Task` resolves.
@template E The type of the rejection reason if the `Task` rejects.
@template F The type of the rejection for the new `Task`, returned by the
`mapFn`.
@param mapFn The function to apply to the rejection reason if the `Task` is
rejected.
*/
mapRejected<F>(mapFn: (e: E) => F): Task<T, F>;
/**
You can think of this like a short-circuiting logical "and" operation on a
{@linkcode Task}. If this `task` resolves, then the output is the task
passed to the method. If this `task` rejects, the result is its rejection
reason.
This is useful when you have another `Task` value you want to provide if and
*only if* the first task resolves successfully – that is, when you need to
make sure that if you reject, whatever else you're handing a `Task` to
*also* gets that {@linkcode Rejected}.
Notice that, unlike in {@linkcode map Task.prototype.map}, the original
`task` resolution value is not involved in constructing the new `Task`.
## Comparison with `andThen`
When you need to perform tasks in sequence, use `andThen` instead: it will
only run the function that produces the next `Task` if the first one
resolves successfully. You should only use `and` when you have two `Task`
instances running concurrently and only need the value from the second if
they both resolve.
## Examples
Using `and` to get new `Task` values from other `Task` values:
```ts
import Task from 'true-myth/task';
let resolvedA = Task.resolve<string, string>('A');
let resolvedB = Task.resolve<string, string>('B');
let rejectedA = Task.reject<string, string>('bad');
let rejectedB = Task.reject<string, string>('lame');
let aAndB = resolvedA.and(resolvedB);
await aAndB;
let aAndRA = resolvedA.and(rejectedA);
await aAndRA;
let raAndA = rejectedA.and(resolvedA);
await raAndA;
let raAndRb = rejectedA.and(rejectedB);
await raAndRb;
expect(aAndB.toString()).toEqual('Task.Resolved("B")');
expect(aAndRA.toString()).toEqual('Task.Rejected("bad")');
expect(raAndA.toString()).toEqual('Task.Rejected("bad")');
expect(raAndRb.toString()).toEqual('Task.Rejected("bad")');
```
Using `and` to get new `Task` values from a `Result`:
```ts
import Task from 'true-myth/task';
let resolved = Task.resolve<string, string>('A');
let rejected = Task.reject<string, string>('bad');
let ok = Result.ok<string, string>('B');
let err = Result.err<string, string>('lame');
let aAndB = resolved.and(ok);
await aAndB;
let aAndRA = resolved.and(err);
await aAndRA;
let raAndA = rejected.and(ok);
await raAndA;
let raAndRb = rejected.and(err);
await raAndRb;
expect(aAndB.toString()).toEqual('Task.Resolved("B")');
expect(aAndRA.toString()).toEqual('Task.Rejected("bad")');
expect(raAndA.toString()).toEqual('Task.Rejected("bad")');
expect(raAndRb.toString()).toEqual('Task.Rejected("bad")');
```
@template U The type of the value for a resolved version of the `other`
`Task`, i.e., the success type of the final `Task` present if the first
`Task` is `Ok`.
@param other The `Task` instance to return if `this` is `Rejected`.
*/
and<U, F = E>(other: Task<U, F> | Result<U, F>): Task<U, E | F>;
/**
Apply a function to the resulting value if a {@linkcode Task} is {@linkcode
Resolved}, producing a new `Task`; or if it is {@linkcode Rejected} return
the rejection reason unmodified.
This differs from `map` in that `thenFn` returns another `Task`. You can use
`andThen` to combine two functions which *both* create a `Task` from an
unwrapped type.
Because it is very common to work with a mix of synchronous and asynchronous
operations, `andThen` also “lifts” a {@linkcode Result} value into a `Task`.
An {@linkcode result.Ok Ok} will produce the same outcome as a `Resolved`
`Task`, and an {@linkcode result.Err Err} will produce the same outcome as a
`Rejected` `Task`.
The [`Promise.prototype.then`][then] method is a helpful comparison: if you
have a `Promise`, you can pass its `then` method a callback which returns
another `Promise`, and the result will not be a *nested* promise, but a
single `Promise`. The difference is that `Promise.prototype.then` unwraps
_all_ layers to only ever return a single `Promise` value, whereas this
method will not unwrap nested `Task`s.
`Promise.prototype.then` also acts the same way {@linkcode map
Task.prototype.map} does, while `Task` distinguishes `map` from `andThen`.
> [!NOTE] `andThen` is sometimes also known as `bind`, but *not* aliased as
> such because [`bind` already means something in JavaScript][bind].
[then]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/then
[bind]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/bind
## Examples
Using `andThen` to construct a new `Task` from a `Task` value:
```ts
import * as task from 'true-myth/task';
const toLengthAsResolved = (s: string) => task.resolve(s.length);
const aResolvedTask = task.resolve('just a string');
const lengthAsResult = await aResolvedTask.andThen(toLengthAsResolved);
console.log(lengthAsResult.toString()); // Ok(13)
const aRejectedTask = task.reject(['srsly', 'whatever']);
const notLengthAsResult = await aRejectedTask.andThen(toLengthAsResolved);
console.log(notLengthAsResult.toString()); // Err(srsly,whatever)
```
Using `andThen` to construct a new `Task` from a `Result` value:
```ts
import * as task from 'true-myth/task';
const toLengthAsResult = (s: string) => ok(s.length);
const aResolvedTask = task.resolve('just a string');
const lengthAsResult = await aResolvedTask.andThen(toLengthAsResult);
console.log(lengthAsResult.toString()); // Ok(13)
const aRejectedTask = task.reject(['srsly', 'whatever']);
const notLengthAsResult = await aRejectedTask.andThen(toLengthAsResult);
console.log(notLengthAsResult.toString()); // Err(srsly,whatever)
```
@template U The type of the value produced by the new `Task` of the `Result`
returned by the `thenFn`.
@param thenFn The function to apply to the wrapped `T` if `maybe` is `Just`.
*/
andThen<U>(thenFn: (t: T) => Task<U, E> | Result<U, E>): Task<U, E>;
andThen<R extends AnyTask | AnyResult>(thenFn: (t: T) => R): Task<ResolvesTo<R>, E | RejectsWith<R>>;
/**
Provide a fallback for a given {@linkcode Task}. Behaves like a logical
`or`: if the `task` value is {@linkcode Resolved}, returns that `task`
unchanged, otherwise, returns the `other` `Task`.
This is useful when you want to make sure that something which takes a
`Task` always ends up getting a {@linkcode Resolved} variant, by supplying a
default value for the case that you currently have an {@linkcode Rejected}.
## Comparison with `orElse`
When you need to run a `Task` in sequence if another `Task` rejects, use
`orElse` instead: it will only run the function that produces the next
`Task` if the first one rejects. You should only use `or` when you have two
`Task` instances running concurrently and only need the value from the
second if the first rejects.
## Examples
Using `or` to get new `Task` values from other `Task` values:
```ts
import Task from 'true-myth/task';
const resolvedA = Task.resolve<string, string>('a');
const resolvedB = Task.resolve<string, string>('b');
const rejectedWat = Task.reject<string, string>(':wat:');
const rejectedHeaddesk = Task.reject<string, string>(':headdesk:');
console.log(resolvedA.or(resolvedB).toString()); // Resolved("a")
console.log(resolvedA.or(rejectedWat).toString()); // Resolved("a")
console.log(rejectedWat.or(resolvedB).toString()); // Resolved("b")
console.log(rejectedWat.or(rejectedHeaddesk).toString()); // Rejected(":headdesk:")
```
Using `or` to get new `Task` values from `Result` values:
```ts
import Task from 'true-myth/task';
import Result from 'true-myth/result';
const resolved = Task.resolve<string, string>('resolved');
const rejected = Task.reject<string, string>('rejected');
const ok = Result.ok<string, string>('ok');
const err = Result.err<string, string>('err');
console.log(resolved.or(ok).toString()); // Resolved("resolved")
console.log(resolved.or(err).toString()); // Resolved("err")
console.log(rejected.or(ok).toString()); // Resolved("ok")
console.log(rejected.or(err).toString()); // Rejected("err")
```
@template F The type wrapped in the `Rejected` case of `other`.
@param other The `Result` to use if `this` is `Rejected`.
@returns `this` if it is `Resolved`, otherwise `other`.
*/
or<F, U = T>(other: Task<U, F> | Result<U, F>): Task<T | U, F>;
/**
Like {@linkcode or}, but using a function to construct the alternative
{@linkcode Task}.
Sometimes you need to perform an operation using the rejection reason (and
possibly also other data in the environment) to construct a new `Task`,
which may itself resolve or reject. In these situations, you can pass a
function (which may be a closure) as the `elseFn` to generate the fallback
`Task<T, E>`. It can then transform the data in the {@linkcode Rejected} to
something usable as an {@linkcode Resolved}, or generate a new `Rejected`
instance as appropriate.
As with {@linkcode andThen}, `orElse` can be used with either `Task` or
{@linkcode Result} values.
Useful for transforming failures to usable data, for trigger retries, etc.
@param elseFn The function to apply to the `Rejection` reason if the `Task`
rejects, to create a new `Task`.
*/
orElse<F>(elseFn: (reason: E) => Task<T, F> | Result<T, F>): Task<T, F>;
orElse<R extends AnyTask | AnyResult>(elseFn: (reason: E) => R): Task<T | ResolvesTo<R>, RejectsWith<R>>;
/**
Allows you to produce a new value by providing functions to operate against
both the {@linkcode Resolved} and {@linkcode Rejected} states once the
{@linkcode Task} resolves.
(This is a workaround for JavaScript’s lack of native pattern-matching.)
## Example
```ts
import Task from 'true-myth/task';
let theTask = new Task<number, Error>((resolve, reject) => {
let value = Math.random();
if (value > 0.5) {
resolve(value);
} else {
reject(new Error(`too low: ${value}`));
}
});
// Note that we are here awaiting the `Promise` returned from the `Task`,
// not the `Task` itself.
await theTask.match({
Resolved: (num) => {
console.log(num);
},
Rejected: (err) => {
console.error(err);
},
});
```
This can both be used to produce side effects (as here) and to produce a
value regardless of the resolution/rejection of the task, and is often
clearer than trying to use other methods. Thus, this is especially
convenient for times when there is a complex task output.
> [!NOTE]
> You could also write the above example like this, taking advantage of how
> awaiting a `Task` produces its inner `Result`:
>
> ```ts
> import Task from 'true-myth/task';
>
> let theTask = new Task<number, Error>((resolve, reject) => {
> let value = Math.random();
> if (value > 0.5) {
> resolve(value);
> } else {
> reject(new Error(`too low: ${value}`));
> }
> });
>
> let theResult = await theTask;
> theResult.match({
> Ok: (num) => {
> console.log(num);
> },
> Err: (err) => {
> console.error(err);
> },
> });
> ```
>
> Which of these you choose is a matter of taste!
@param matcher A lightweight object defining what to do in the case of each
variant.
*/
match<A>(matcher: Matcher<T, E, A>): Promise<A>;
/**
Attempt to run this {@linkcode Task} to completion, but stop if the passed
{@linkcode Timer}, or one constructed from a passed time in milliseconds,
elapses first.
If this `Task` and the duration happen to have the same duration, `timeout`
will favor this `Task` over the timeout.
@param timerOrMs A {@linkcode Timer} or a number of milliseconds to wait for
this task before timing out.
@returns A `Task` which has the resolution value of `this` or a `Timeout`
if the timer elapsed.
*/
timeout(timerOrMs: Timer | number): Task<T, E | Timeout>;
/**
Get the underlying `Promise`. Useful when you need to work with an
API which *requires* a `Promise`, rather than a `PromiseLike`.
Note that this maintains the invariants for a `Task` *up till the point you
call this function*. That is, because the resulting promise was managed by a
`Task`, it always resolves successfully to a `Result`. However, calling then
`then` or `catch` methods on that `Promise` will produce a *new* `Promise`
for which those guarantees do not hold.
> [!IMPORTANT]
> If the resulting `Promise` ever rejects, that is a ***BUG***, and you
> should [open an issue](https://github.com/true-myth/true-myth/issues) so
> we can fix it!
*/
toPromise(): Promise<Result<T, E>>;
/**
Given a nested `Task`, remove one layer of nesting.
For example, given a `Task<Task<string, E2>, E1>`, the resulting type after
using this method will be `Task<string, E1 | E2>`.
## Note
This method only works when the value wrapped in `Task` is another `Task`.
If you have a `Task<string, E>` or `Task<number, E>`, this method won't
work. If you have a `Task<Task<string, E2>, E1>`, then you can call
`.flatten()` to get back a `Task<string, E1 | E2>`.
## Examples
```ts
import * as task from 'true-myth/task';
const nested = task.resolve(task.resolve('hello'));
const flattened = nested.flatten(); // Task<string, never>
await flattened;
console.log(flattened); // `Resolved('hello')`
const nestedError = task.resolve(task.reject('inner error'));
const flattenedError = nestedError.flatten(); // Task<never, string>
await flattenedError;
console.log(flattenedError); // `Rejected('inner error')`
const errorNested = task.reject<Task<string, string>, string>('outer error');
const flattenedOuter = errorNested.flatten(); // Task<string, string>
await flattenedOuter;
console.log(flattenedOuter); // `Rejected('outer error')`
```
*/
flatten<A, F, G>(this: Task<Task<A, F>, G>): Task<A, F | G>;
}
/**
An unknown {@linkcode Task}. This is a private type utility; it is only
exported for the sake of internal tests.
@internal
*/
export type AnyTask = Task<unknown, unknown>;
export type TaskTypesFor<A extends readonly AnyTask[]> = {
resolution: {
-readonly [P in keyof A]: ResolvesTo<A[P]>;
};
rejection: {
-readonly [P in keyof A]: RejectsWith<A[P]>;
};
};
/**
The resolution type for a given {@linkcode Task}.
@internal
*/
export type ResolvesTo<T extends AnyTask | AnyResult> = TypesFor<T>['resolution'];
/**
The rejection type for a given {@linkcode Task}
@internal
*/
export type RejectsWith<T extends AnyTask | AnyResult> = TypesFor<T>['rejection'];
/**
Create a {@linkcode Task} which will resolve to the number of milliseconds the
timer waited for that time elapses. (In other words, it safely wraps the
[`setTimeout`][setTimeout] function.)
[setTimeout]: https://developer.mozilla.org/en-US/docs/Web/API/Window/setTimeout
This can be used as a “timeout” by calling it in conjunction any of the
{@linkcode Task} helpers like {@linkcode all}, {@linkcode race}, and so on. As
a convenience to use it as a timeout for another task, you can also combine it
with the {@linkcode Task.timeout} instance method or the standalone
{@linkcode timeout} function.
Provides the requested duration of the timer in case it is useful for working
with multiple timers.
@param ms The number of milliseconds to wait before resolving the `Task`.
@returns a Task which resolves to the passed-in number of milliseconds.
*/
export declare function timer(ms: number): Timer;
/**
A type utility for mapping an input array of tasks into the appropriate output
for `all`.
@internal
*/
export type All<A extends readonly AnyTask[]> = Task<[
...TaskTypesFor<A>['resolution']
], TaskTypesFor<A>['rejection'][number]>;
/**
Given an array of tasks, return a new `Task` that resolves once all tasks
successfully resolve or any task rejects.
## Examples
Once all tasks resolve:
```ts
import { all, timer } from 'true-myth/task';
let allTasks = all([
timer(10),
timer(100),
timer(1_000),
]);
let result = await allTasks;
console.log(result.toString()); // [Ok(10,100,1000)]
```
If any tasks do *not* resolve:
```ts
let { task: willReject, reject } = Task.withResolvers<never, string>();
let allTasks = all([
timer(10),
timer(20),
willReject,
]);
reject("something went wrong");
let result = await allTasks;
console.log(result.toString()); // Err("something went wrong")
```
@param tasks The list of tasks to wait on.
@template A The type of the array or tuple of tasks.
*/
export declare function all(tasks: []): Task<[], never>;
export declare function all<const A extends readonly AnyTask[]>(tasks: A): All<A>;
/**
@internal
*/
export type Settled<A extends readonly AnyTask[]> = {
-readonly [P in keyof A]: Result<ResolvesTo<A[P]>, RejectsWith<A[P]>>;
};
/**
Given an array of tasks, return a new {@linkcode Task} which resolves once all
of the tasks have either resolved or rejected. The resulting `Task` is a tuple
or array corresponding exactly to the tasks passed in, either resolved or
rejected.
## Example
Given a mix of resolving and rejecting tasks:
```ts
let settledTask = allSettled([
Task.resolve<string, number>("hello"),
Task.reject<number, boolean>(true),
Task.resolve<{ fancy: boolean }>, Error>({ fancy: true }),
]);
let output = await settledTask;
if (output.isOk) { // always true, not currently statically knowable
for (let result of output.value) {
console.log(result.toString());
}
}
```
The resulting output will be:
```
Ok("hello"),
Err(true),
Ok({ fancy: true }),
```
@param tasks The tasks to wait on settling.
@template A The type of the array or tuple of tasks.
*/
export declare function allSettled<const A extends readonly AnyTask[]>(tasks: A): Task<Settled<A>, never>;
/**
Given an array of tasks, return a new {@linkcode Task} which resolves once
_any_ of the tasks resolves successfully, or which rejects once _all_ the
tasks have rejected.
## Examples
When any item resolves:
```ts
import { any, timer } from 'true-myth/task';
let anyTask = any([
timer(20),
timer(10),
timer(30),
]);
let result = await anyTask;
console.log(result.toString()); // Ok(10);
```
When all items reject:
```ts
import Task, { timer } from 'true-myth/task';
let anyTask = any([
timer(20).andThen((time) => Task.reject(`${time}ms`)),
timer(10).andThen((time) => Task.reject(`${time}ms`)),
timer(30).andThen((time) => Task.reject(`${time}ms`)),
]);
let result = await anyTask;
console.log(result.toString()); // Err(AggregateRejection: `Task.any`: 10ms,20ms,30ms)
```
The order in the resulting `AggregateRejection` is guaranteed to be stable and
to match the order of the tasks passed in.
@param tasks The set of tasks to check for any resolution.
@returns A Task which is either {@linkcode Resolved} with the value of the
first task to resolve, or {@linkcode Rejected} with the rejection reasons
for all the tasks passed in in an {@linkcode AggregateRejection}. Note that
the order of the rejection reasons is not guaranteed.
@template A The type of the array or tuple of tasks.
*/
export declare function any(tasks: []): Task<never, AggregateRejection<[]>>;
export declare function any<const A extends readonly AnyTask[]>(tasks: A): Task<TaskTypesFor<A>['resolution'][number], AggregateRejection<[...TaskTypesFor<A>['rejection']]>>;
/**
Given an array of tasks, produce a new {@linkcode Task} which will resolve or
reject with the resolution or rejection of the *first* task which settles.
## Example
```ts
import Task, { race } from 'true-myth/task';
let { task: task1, resolve } = Task.withResolvers();
let task2 = new Task((_resolve) => {});
let task3 = new Task((_resolve) => {});
resolve("Cool!");
let theResult = await race([task1, task2, task3]);
console.log(theResult.toString()); // Ok("Cool!")
```
@param tasks The tasks to race against each other.
@template A The type of the array or tuple of tasks.
*/
export declare function race(tasks: []): Task<never, never>;
export declare function race<A extends readonly AnyTask[]>(tasks: A): Task<TaskTypesFor<A>['resolution'][number], TaskTypesFor<A>['rejection'][number]>;
/**
An error type produced when {@linkcode any} produces any rejections. All
rejections are aggregated into this type.
> [!NOTE]
> This error type is not allowed to be subclassed.
@template E The type of the rejection reasons.
*/
export declare class AggregateRejection<E extends unknown[]> extends Error {
readonly errors: E;
readonly name = "AggregateRejection";
constructor(errors: E);
toString(): string;
}
/**
A {@linkcode Task Task<T, E>} that has not yet resolved.
@template T The type of the value when the `Task` resolves successfully.
@template E The type of the rejection reason when the `Task` rejects.
@group Task Variants
*/
export interface Pending<T, E> extends Omit<TaskImpl<T, E>, 'value' | 'reason'> {
get isPending(): true;
get isResolved(): false;
get isRejected(): false;
get state(): typeof State.Pending;
}
/**
A {@linkcode Task Task<T, E>} that has resolved. Its `value` is of type `T`.
@template T The type of the value when the `Task` resolves successfully.
@template E The type of the rejection reason when the `Task` rejects.
@group Task Variants
*/
export interface Resolved<T, E> extends Omit<TaskImpl<T, E>, 'reason'> {
get isPending(): false;
get isResolved(): true;
get isRejected(): false;
get state(): typeof State.Resolved;
get value(): T;
}
/**
A {@linkcode Task Task<T, E>} that has rejected. Its `reason` is of type `E`.
@template T The type of the value when the `Task` resolves successfully.
@template E The type of the rejection reason when the `Task` rejects.
@group Task Variants
*/
export interface Rejected<T, E> extends Omit<TaskImpl<T, E>, 'value'> {
get isPending(): false;
get isResolved(): false;
get isRejected(): true;
get state(): typeof State.Rejected;
get reason(): E;
}
export declare const State: {
readonly Pending: "Pending";
readonly Resolved: "Resolved";
readonly Rejected: "Rejected";
};
type State = (typeof State)[keyof typeof State];
/** Type returned by calling {@linkcode Task.withResolvers} */
export type WithResolvers<T, E> = {
task: Task<T, E>;
resolve: (value: T) => void;
reject: (reason: E) => void;
};
/**
A lightweight object defining how to handle each outcome state of a
{@linkcode Task}.
*/
export type Matcher<T, E, A> = {
Resolved: (value: T) => A;
Rejected: (reason: E) => A;
};
/**
The error thrown when an error is thrown in the executor passed to {@linkcode
Task.constructor}. This error class exists so it is clear exactly what went
wrong in that case.
@group Errors
*/
export declare class TaskExecutorException extends Error {
name: string;
constructor(originalError: unknown);
}
/**
An error thrown when the `Promise<Result<T, E>>` passed to
{@link fromUnsafePromise} rejects.
@group Errors
*/
export declare class UnsafePromise extends Error {
readonly name = "TrueMyth.Task.UnsafePromise";
constructor(unhandledError: unknown);
}
export declare class InvalidAccess extends Error {
readonly name = "TrueMyth.Task.InvalidAccess";
constructor(field: 'value' | 'reason', state: State);
}
/**
The public interface for the {@linkcode Task} class *as a value*: a
constructor and the associated static properties.
*/
export interface TaskConstructor {
/**
Construct a new `Task`, using callbacks to wrap APIs which do not natively
provide a `Promise`.
This is identical to the [Promise][promise] constructor, with one very
important difference: rather than producing a value upon resolution and
throwing an exception when a rejection occurs like `Promise`, a `Task`
always “succeeds” in producing a usable value, just like {@linkcode Result}
for synchronous code.
[promise]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/Promise
For constructing a `Task` from an existing `Promise`, see:
- {@linkcode fromPromise}
- {@linkcode safelyTry}
- {@linkcode tryOr}
- {@linkcode tryOrElse}
For constructing a `Task` immediately resolved or rejected with given
values, see {@linkcode Task.resolve} and {@linkcode Task.reject}
respectively.
@param executor A function which the constructor will execute to manage
the lifecycle of the `Task`. The executor in turn has two functions as
parameters: one to call on resolution, the other on rejection.
*/
new <T, E>(executor: (resolve: (value: T) => void, reject: (reason: E) => void) => void): Task<T, E>;
/**
Construct a `Task` which is already resolved. Useful when you have a value
already, but need it to be available in an API which expects a `Task`.
@group Constructors
*/
resolve<T extends Unit, E = never>(): Task<Unit, E>;
/**
Construct a `Task` which is already resolved. Useful when you have a value
already, but need it to be available in an API which expects a `Task`.
@group Constructors
*/
resolve<T, E = never>(value: T): Task<T, E>;
/**
Construct a `Task` which is already rejected. Useful when you have an error
already, but need it to be available in an API which expects a `Task`.
@group Constructors
*/
reject<T = never, E extends {} = {}>(): Task<T, Unit>;
/**
Construct a `Task` which is already rejected. Useful when you have an error
already, but need it to be available in an API which expects a `Task`.
@group Constructors
*/
reject<T = never, E = unknown>(reason: E): Task<T, E>;
/**
Create a pending `Task` and supply `resolveWith` and `rejectWith` helpers,
similar to the [`Promise.withResolvers`][pwr] static method, but producing a
`Task` with the usual safety guarantees.
[pwr]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/withResolvers
## Examples
### Resolution
```ts
let { task, resolveWith, rejectWith } = Task.withResolvers<string, Error>();
resolveWith("Hello!");
let result = await task.map((s) => s.length);
let length = result.unwrapOr(0);
console.log(length); // 5
```
### Rejection
```ts
let { task, resolveWith, rejectWith } = Task.withResolvers<string, Error>();
rejectWith(new Error("oh teh noes!"));
let result = await task.mapRejection((s) => s.length);
let errLength = result.isErr ? result.error : 0;
console.log(errLength); // 5
```
@group Constructors
*/
withResolvers<T, E>(): WithResolvers<T, E>;
}
/**
A `Task` is a type safe asynchronous computation.
You can think of a `Task<T, E>` as being basically a `Promise<Result<T, E>>`,
because it *is* a `Promise<Result<T, E>>` under the hood, but with two main
differences from a “normal” `Promise`:
1. A `Task` *cannot* “reject”. All errors must be handled. This means that,
like a {@linkcode Result}, it will *never* throw an error if used in
strict TypeScript.
2. Unlike `Promise`, `Task` robustly distinguishes between `map` and `andThen`
operations.
`Task` also implements JavaScript’s `PromiseLike` interface, so you can
`await` it; when a `Task<T, E>` is awaited, it produces a {@linkcode result
Result<T, E>}.
@class
*/
export declare const Task: TaskConstructor;
/**
A `Task` is a type safe asynchronous computation.
You can think of a `Task<T, E>` as being basically a `Promise<Result<T, E>>`,
because it *is* a `Promise<Result<T, E>>` under the hood, but with two main
differences from a “normal” `Promise`:
1. A `Task` *cannot* “reject”. All errors must be handled. This means that,
like a {@linkcode Result}, it will *never* throw an error if used in
strict TypeScript.
2. Unlike `Promise`, `Task` robustly distinguishes between `map` and `andThen`
operations.
`Task` also implements JavaScript’s `PromiseLike` interface, so you can
`await` it; when a `Task<T, E>` is awaited, it produces a {@linkcode result
Result<T, E>}.
@class
@template T The type of the value when the `Task` resolves successfully.
@template E The type of the rejection reason when the `Task` rejects.
*/
export type Task<T, E> = Pending<T, E> | Resolved<T, E> | Rejected<T, E>;
export default Task;
declare const PhantomData: unique symbol;
/** @internal */
export declare class Phantom<T extends PropertyKey> {
private readonly [PhantomData];
}
/**
A {@linkcode Task} specialized for use with {@linkcode timeout} or other
methods or functions which want to know they are using.
> [!NOTE]
> This type has zero runtime overhead, including for construction: it is just
> a `Task` with additional *type information*.
*/
export type Timer = Task<number, never>;
/**
An `Error` type representing a timeout, as when a {@linkcode Timer} elapses.
*/
declare class Timeout extends Error {
#private;
readonly ms: number;
get duration(): number;
constructor(ms: number);
}
export type { Timeout };
/** Standalone function version of {@linkcode Task.resolve} */
export declare const resolve: {
<T extends Unit, E = never>(): Task<Unit, E>;
<T_1, E_1 = never>(value: T_1): Task<T_1, E_1>;
};
/** Standalone function version of {@linkcode Task.reject} */
export declare const reject: {
<T = never, E extends {} = {}>(): Task<T, Unit>;
<T_1 = never, E_1 = unknown>(reason: E_1): Task<T_1, E_1>;
};
/** Standalone function version of {@linkcode Task.withResolvers} */
export declare const withResolvers: <T, E>() => WithResolvers<T, E>;
/**
Produce a {@linkcode Task Task<T, unknown>} from a [`Promise`][mdn-promise].
[mdn-promise]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise
To handle the error case and produce a `Task<T, E>` instead, use the overload
the overload which accepts an `onRejection` handler instead.
> [!IMPORTANT]
> This does not (and by definition cannot) handle errors that happen during
> construction of the `Promise`, because those happen before this is called.
> See {@linkcode safelyTry}, {@linkcode tryOr}, or
> {@linkcode tryOrElse} for alternatives which accept a callback for
> constructing a promise and can therefore handle errors thrown in the call.
@param promise The promise from which to create the `Task`.
@template T The type the `Promise` would resolve to, and thus that the `Task`
will also resolve to if the `Promise` resolves.
@group Constructors
*/
export declare function fromPromise<T>(promise: Promise<T>): Task<T, unknown>;
/**
Produce a {@linkcode Task Task<T, E>} from a [`Promise`][mdn-promise], using a
.
[mdn-promise]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise
To absorb all errors/rejections as `unknown`, use the overload without an
`onRejection` handler instead.
> [!IMPORTANT]
> This does not (and by definition cannot) handle errors that happen during
> construction of the `Promise`, because those happen before this is called.
> See {@linkcode safelyTry}, {@linkcode tryOr}, or
> {@linkcode tryOrElse} for alternatives which accept a callback for
> constructing a promise and can therefore handle errors thrown in the call.
@param promise The promise from which to create the `Task`.
@param onRejection Transform errors from `unknown` to a known error type.
@template T The type the `Promise` would resolve to, and thus that the `Task`
will also resolve to if the `Promise` resolves.
@template E The type of a rejected `Task` if the promise rejects.
@group Constructors
*/
export declare function fromPromise<T, E>(promise: Promise<T>, onRejection: (reason: unknown) => E): Task<T, E>;
/**
Build a {@linkcode Task Task<T, E>} from a {@linkcode Result Result<T, E>}.
> [!IMPORTANT]
> This does not (and by definition cannot) handle errors that happen during
> construction of the `Result`, because those happen before this is called.
> See {@linkcode tryOr} and {@linkcode tryOrElse} as well as the corresponding
> {@linkcode "result".tryOr result.tryOr} and {@linkcode "result".tryOrElse
> result.tryOrElse} methods for synchronous functions.
## Examples
Given an {@linkcode "result".Ok Ok<T, E>}, `fromResult` will produces a
{@linkcode Resolved Resolved<T, E>} task.
```ts
import { fromResult } from 'true-myth/task';
import { ok } from 'true-myth/result';
let successful = fromResult(ok("hello")); // -> Resolved("hello")
```
Likewise, given an `Err`, `fromResult` will produces a {@linkcode Rejected}
task.
```ts
import { fromResult } from 'true-myth/task';
import { err } from 'true-myth/result';
let successful = fromResult(err("uh oh!")); // -> Rejected("uh oh!")
```
It is often clearest to access the function via a namespace-style import:
```ts
import * as task from 'true-myth/task';
import { ok } from 'true-myth/result';
let theTask = task.fromResult(ok(123));
```
As an alternative, it can be useful to rename the import:
```ts
import { fromResult: taskFromResult } from 'true-myth/task';
import { err } from 'true-myth/result';
let theTask = taskFromResult(err("oh no!"));
```
*/
export declare function fromResult<T, E>(result: Result<T, E>): Task<T, E>;
/**
Produce a `Task<T, E>` from a promise of a {@linkcode Result Result<T, E>}.
> [!WARNING]
> This constructor assumes you have already correctly handled the promise
> rejection state, presumably by mapping it into the wrapped `Result`. It is
> *unsafe* for this promise ever to reject! You should only ever use this
> with `Promise<Result<T, E>>` you have created yourself (including via a
> `Task`, of course).
>
> For any other `Promise<Result<T, E>>`, you should first attach a `catch`
> handler which will also produce a `Result<T, E>`.
>
> If you call this with an unmanaged `Promise<Result<T, E>>`, that is, one
> that has *not* correctly set up a `catch` handler, the rejection will
> throw an {@linkcode UnsafePromise} error that will ***not*** be catchable
> by awaiting the `Task` or its original `Promise`. This can cause test
> instability and unpredictable behavior in your application.
@param promise The promise from which to create the `Task`.
@group Constructors
*/
export declare function fromUnsafePromise<T, E>(promise: Promise<Result<T, E>>): Task<T, E>;
/**
Given a function which takes no arguments and returns a `Promise`, return a
{@linkcode Task Task<T, unknown>} for the result of invoking that function.
This safely handles functions which fail synchronously or asynchronously, so
unlike {@linkcode fromPromise} is safe to use with values which may throw
errors _before_ producing a `Promise`.
## Examples
```ts
import { safelyTry } from 'true-myth/task';
function throws(): Promise<T> {
throw new Error("Uh oh!");
}
// Note: passing the function by name, *not* calling it.
let theTask = safelyTry(throws);
let theResult = await theTask;
console.log(theResult.toString()); // Err(Error: Uh oh!)
```
@param fn A function which returns a `Promise` when called.
@returns A `Task` which resolves to the resolution value of the promise or
rejects with the rejection value of the promise *or* any error thrown while
invoking `fn`.
*/
export declare function safelyTry<T>(fn: () => Promise<T>): Task<T, unknown>;
/**
Given a function which takes no arguments and returns a `Promise` and a value
of type `E` to use as the rejection if the `Promise` rejects, return a
{@linkcode Task Task<T, E>} for the result of invoking that function. This
safely handles functions which fail synchronously or asynchronously, so unlike
{@linkcode fromPromise} is safe to use with values which may throw errors
_before_ producing a `Promise`.
## Examples
```ts
import { tryOr } from 'true-myth/task';
function throws(): Promise<number> {
throw new Error("Uh oh!");
}
// Note: passing the function by name, *not* calling it.
let theTask = tryOr("fallback", throws);
let theResult = await theTask;
if (theResult.isErr) {
console.error(theResult.error); // "fallback"
}
```
You can also write this in “curried” form, passing just the fallback value and
getting back a function which accepts the:
```ts
import { tryOr } from 'true-myth/task';
function throws(): Promise<number> {
throw new Error("Uh oh!");
}
// Note: passing the function by name, *not* calling it.
let withFallback = tryOr<number, string>("fallback");
let theResult = await withFallback(throws);
if (theResult.isErr) {
console.error(theResult.error); // "fallback"
}
```
Note that in the curried form, you must specify the expected `T` type of the
resulting `Task`, or else it will always be `unknown`.
@param rejection The value to use if the `Promise` rejects.
@param fn A function which returns a `Promise` when called.
@returns A `Task` which resolves to the resolution value of the promise or
rejects with the rejection value of the promise *or* any error thrown while
invoking `fn`.
*/
export declare function tryOr<T, E>(rejection: E, fn: () => Promise<T>): Task<T, E>;
export declare function tryOr<T, E>(rejection: E): (fn: () => Promise<T>) => Task<T, E>;
/**
An alias for {@linkcode tryOr} for ease of migrating from v8.x to v9.x.
> [!TIP]
> You should switch to {@linkcode tryOr}. We expect to deprecate and remove
> this alias at some point!
*/
export declare const safelyTryOr: typeof tryOr;
/**
An alias for {@linkcode tryOrElse} for ease of migrating from v8.x to v9.x.
> [!TIP]
> You should switch to {@linkcode tryOrElse}. We expect to deprecate and
> remove this alias at some point!
*/
export declare const safelyTryOrElse: typeof tryOrElse;
/**
Given a function which takes no arguments and returns a `PromiseLike` and a
function which accepts an `unknown` rejection reason and transforms it into a
known rejection