You can think of this like a short-circuiting logical "and" operation on a
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 Rejected
.
Notice that, unlike in Task.prototype.map
, the original
task
resolution value is not involved in constructing the new Task
.
let resolvedA = Task.resolved<string, string>('A');
let resolvedB = Task.resolved<string, string>('B');
let rejectedA = Task.rejected<string, string>('bad');
let rejectedB = Task.rejected<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")');
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
.
Apply a function to the resulting value if a Task
is Resolved
, producing a new Task
; or if it is 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.
The Promise.prototype.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 Task.prototype.map
does, while Task
distinguishes map
from andThen
.
andThen
is sometimes also known as bind
, but not aliased as
such because bind
already means something in JavaScript.
import Task from 'true-myth/task';
const toLengthAsResult = (s: string) => ok(s.length);
const aResolvedTask = Task.resolved('just a string');
const lengthAsResult = await aResolvedTask.andThen(toLengthAsResult);
console.log(lengthAsResult.toString()); // Ok(13)
const aRejectedTask = Task.rejected(['srsly', 'whatever']);
const notLengthAsResult = await aRejectedTask.andThen(toLengthAsResult);
console.log(notLengthAsResult.toString()); // Err(srsly,whatever)
The type of the value produced by the new Task
of the Result
returned by the thenFn
.
Map over a 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 Rejected
without modification.
map
works a lot like Array.prototype.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.
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
.
import Task from 'true-myth/task';
const double = n => n * 2;
const aResolvedTask = Task.resolved(12);
const mappedResolved = aResolvedTask.map(double);
let resolvedResult = await aResolvedTask;
console.log(resolvedResult.toString()); // Ok(24)
const aRejectedTask = Task.rejected("nothing here!");
const mappedRejected = map(double, aRejectedTask);
let rejectedResult = await aRejectedTask;
console.log(rejectedResult.toString()); // Err("nothing here!")
Map over a Task
, exactly as in 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.
import Task from 'true-myth/task';
const extractReason = (err: { code: number, reason: string }) => err.reason;
const aResolvedTask = Task.resolved(12);
const mappedResolved = aResolvedTask.mapErr(extractReason);
console.log(mappedOk)); // Ok(12)
const aRejectedTask = Task.rejected({ code: 101, reason: 'bad file' });
const mappedRejection = await aRejectedTask.map(extractReason);
console.log(toString(mappedRejection)); // Err("bad file")
Allows you to produce a new value by providing functions to operate against
both the Resolved
and Rejected
states once the
Task
resolves.
(This is a workaround for JavaScript’s lack of native pattern-matching.)
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.
You could also write the above example like this, taking advantage of how
awaiting a Task
produces its inner Result
:
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!
Provide a fallback for a given Task
. Behaves like a logical
or
: if the task
value is 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 Resolved
variant, by supplying a
default value for the case that you currently have an Rejected
.
import Task from 'true-utils/task';
const resolvedA = Task.resolved<string, string>('a');
const resolvedB = Task.resolved<string, string>('b');
const rejectedWat = Task.rejected<string, string>(':wat:');
const rejectedHeaddesk = Task.rejected<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:")
The type wrapped in the Rejected
case of other
.
this
if it is Resolved
, otherwise other
.
Like or
, but using a function to construct the alternative
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 Rejected
to
something usable as an Resolved
, or generate a new Rejected
instance as appropriate.
Useful for transforming failures to usable data, for trigger retries, etc.
Attaches callbacks for the resolution and/or rejection of the Promise.
A Promise for the completion of which ever callback is executed.
Attempt to run this Task
to completion, but stop if the passed
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.
A Task
which has the resolution value of this
or a Timeout
if the timer elapsed.
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.
If the resulting Promise
ever rejects, that is a BUG, and you
should open an issue so
we can fix it!
A
Task<T, E>
that has resolved. Itsvalue
is of typeT
.