UNPKG

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
/** 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