Creating Tasks

The following functions are used to create Tasks. They can be used from either the Task class (for example Task.of) or imported directly from the library as a function (simply, of).

Many of these methods also exist on the Task instance. They exist here in their pure form in the hopes that the proposed Pipeline operator ever makes its way into the language.

of

Creates a task that will always succeed with the given value. Also aliased as succeed. Similar in use to Promise.resolve.

const task: Task<never, number> = Task.of(5)

empty

Creates a task that will always succeed a result of void (undefined). Useful for chaining tasks which create side-effects, but don't return a useful value.

const task: Task<never, void> = Task.empty()

succeedIn

Creates a task that will always succeed with the given value, but will take some number of milliseconds before it does. Useful for simulating latency or inserting timeouts in a task chain.

const task: Task<never, number> = Task.succeedIn(100, 5)

succeedBy

Creates a task that will always succeed with the result of a function. Useful for bringing global state or side-effects into a task chain.

const task: Task<never, number> = Task.succeedBy(() => 5)

fail

Creates a task that will always fail with the given error. Similar in use to Promise.reject.

const task: Task<string, never> = Task.fail("error")

failIn

Creates a task that will always fail with the given error, but will take some number of milliseconds before it does. Useful for simulating latency or inserting timeouts in a task chain.

const task: Task<string, never> = Task.failIn(100, "error")

all

Creates a task that will always run an array of tasks in parallel. The result in a new task which returns the successful results as an array, in the same order as the tasks were given. If any task fails, the resulting task will fail with that error.

All task error and value types must be the same. If you need different types for the items of the array, consider using ap instead.

Works similarly to Promise.all.

const task: Task<never, number[]> = Task.all([of(5), of(10)])

allSuccesses

Creates a task that will always run an array of tasks in parallel. The result in a new task which returns the successful results as an array, in the same order as the tasks were given. Failed tasks will be excluded.

This can never fail, only return an empty array. Unliked Task.all, the resulting array is in order of success, not initial input order.

const task: Task<never, number[]> = Task.allSuccesses([fail("Err"), of(10)])

sequence

Creates a task that will always run an array of tasks serially. The result in a new task which returns the successful results as an array, in the same order as the tasks were given. If any task fails, the resulting task will fail with that error.

All task error and value types must be the same. If you need different types for the items of the array, consider simply chaining the tasks together in order.

Additionally, a second parameter can be added to allow concurrency. The default concurreny is 1 task at a time, but may be increased. Setting this concurrency to Infinity is the same a Task.all.

const task: Task<never, number[]> = Task.sequence([of(5), of(10)])

firstSuccess

Creates a task that will always run an array of tasks in parallel and return the value of the first successful task. If all tasks fail, the error result will be an array of the error results from each task in the same order as they were given.

const task: Task<string[], number> = Task.firstSuccess([fail("error"), of(10)])

never

Creates a task that will never succeed or fail. Uses the TypeScript never type which allows the type checker to detect impossible cases and warn the developer at compile time.

const task: Task<never, never> = Task.never()

fromPromise

Converts a Promise into a Task. Because Promise start in a "pending" state, that process will already be running before the Task is forked. This means that if two tasks chain off this one, the initial Promise (say, a web request) will only happen once. Consider using fromLazyPromise instead to bring Promise more in line with the lazy Task philosophy.

Promise's do not track an error type (one of the reasons Tasks are more powerful) so the resulting Task is unable to infer the error type as well. It is recommended to pass it in as a generic.

const task: Task<unknown, Response> = Task.fromPromise(fetch(URL))

fromPromises

Converts an array of Promises into an array of Tasks and joins them with Task.all

Promise's do not track an error type (one of the reasons Tasks are more powerful) so the resulting Task is unable to infer the error type as well. It is recommended to pass it in as a generic.

const task: Task<unknown, Response> = Task.fromPromises(fetch(URL))

fromLazyPromise

Given a function which returns a Promise, turn that into a Task. This allows the Promise not to start until the Task forks (following the lazy philosophy of the rest of the library). This also means if two tasks chain from this one, the promise creating function will be called twice. See onlyOnce if you wish to avoid this.

Promise's do not track an error type (one of the reasons Tasks are more powerful) so the resulting Task is unable to infer the error type as well. It is recommended to pass it in as a generic.

const task: Task<unknown, Response> = Task.fromLazyPromise(() => fetch(URL))

wrapPromiseCreator

Given a function which returns a Promise, create a new function which given the same arguments will now return a Task instead.

const taskFetch = wrapPromiseCreator(fetch)
const task: Task<unknown, Response> = taskFetch(URL)

race

Creates a task that will always run an array of tasks in parallel. The first task to finish is the resulting error or value. Useful for implementing network request timeouts by racing a task which fails in x milliseconds and a task which makes the request.

Works similarly to Promise.race.

const task: Task<never, number> = Task.race([
  succeedIn(100, 5),
  succeedIn(10, -5),
])

external

Creates a Task which can be controlled externally by providing reject and resolve methods. Must provide error and value types as generics.

Useful for integrating with callback based libraries and APIs.

const task = Task.external<never, number>()

try {
  attemptSomething()
  task.resolve(5)
} catch (e) {
  task.reject(e)
}

emitter

Creates a Task that wraps a callback. Also returns an emit callback which when called, executes the callback. If the callback throws an exception, that is the error response of the task. Otherwise the returned value of the callback is considered a success.

Must provide error and value types as generics.

Useful for integrating with callback based libraries and APIs. Provides automatic exception handling.

const [task, emit] = Task.emitter<never, string>((e: Event) => {
  attemptSomething()

  return e.type
})

window.onclick = e => {
  emit(e)
}

map2

Given two tasks, run the successful values through a mapping function to combine them into a new output. Unlike Task.all, this allows each task to be of a different type.

The function must be curried. That is, each parameter is handled one at a time. A version of this function which is not curried is available as zipWith.

const task: Task<never, [number, string]> = Task.map2(
  a => b => [a, b],
  Task.of(5),
  Task.of("Hello"),
)

map3

Given three tasks, run the successful values through a mapping function to combine them into a new output. Unlike Task.all, this allows each task to be of a different type.

The function must be curried. That is, each parameter is handled one at a time.

const task: Task<never, [number, string, boolean]> = Task.map3(
  a => b => c => [a, b, c],
  Task.of(5),
  Task.of("Hello"),
  Task.of(true),
)

map4

Given four tasks, run the successful values through a mapping function to combine them into a new output. Unlike Task.all, this allows each task to be of a different type.

The function must be curried. That is, each parameter is handled one at a time.

If you need to operate on more than 4 tasks, consider using ap which can combine an arbitrary number of tasks using a mapping function.

const task: Task<never, [number, string, boolean, Set<string>]> = Task.map4(
  a => b => c => d => [a, b, c, d],
  Task.of(5),
  Task.of("Hello"),
  Task.of(true),
  Task.of(new Set(["hi"])),
)

loop

Allows the construction of a recursive, asynchrous loop. Given an initial starting value, call the currrent loop function and return a Task that contains either a LoopBreak or LoopContinue instance. The instance holds on to the current value. Will loop until it encounters a LoopBreak.

This is a simplified version of what some will accomplish with a Array.prototype.reduce which uses the current Promise as it's value.

// Count to six but wait 100ms between each step.
const task: Task<never, number> = Task.loop(num => {
  if (num > 5) {
    return Task.wait(100).forward(new LoopBreak(num))
  }

  return Task.wait(100).forward(new LoopContinue(num + 1))
}, 1)

reduce

Works exactly like Array.prototype.reduce, but asynchronously. The return value of each reducer must return a Task.

// Count to six but wait 100ms between each step.
const task: Task<never, number> = Task.reduce(
  sum => Task.succeedIn(100, sum + 1),
  0,
  [1, 2, 3, 4, 5, 6],
)

zip

Given two tasks, return a new Task which succeeds with a 2-tuple of their successful results.

const task: Task<never, [number, string]> = Task.zip(
  Task.of(5),
  Task.of("Hello"),
)

zipWith

Given two tasks, return a new Task which succeeds by running the successful results through a mapping function. Very similar to map2, but zipWith uses 1 parameter per successful Task. map2 uses a curried mapping function.

const task: Task<never, [number, string]> = Task.zip(
  Task.of(5),
  Task.of("Hello"),
)

flatten

Given a task which succeeds with another task, flatten into a single task which succeeds with the result of that nested task. Often this can be avoided by using chain.

const task: Task<never, number> = Task.flatten(Task.of(Task.of(5)))

ap

The applicative. If you know what that means, you'll be excited. If not, it is fine. This is a low level tool that helps build more complex features.

ap starts with a Task which succeeds containing a function. Subsequence compositions of ap will each provide a task. The success of all those tasks will be given to the initial task which resulted in a function. This is a type safe way of running map on an arbitrary number of tasks. The function specifies its arguments, which must equal the number of ap chains. Similar to Task.all, but with 1 parameter per task, rather than an array, and it works with different task success types, rather than requiring all tasks succeed with the same type.

Also allows the definition of the mapping function to be asychronous because it is also inside a Task.

Without easy function composition in Javascript, for readability we recommend using the instance version .ap rather than the nested calls the pure function requires.

const task: Task<never, number> = Task.ap(
  Task.ap(
    Task.ap(
      Task.of((a, b, c) => a + b + c),
      succeed(10) /* a */,
    ),
    succeed(50) /* b */,
  ),
  succeed(100) /* c */,
)

Last updated