@halo-lab/future
v0.0.6
Published
A better Promise
Readme
Future
It is a Promise compatible type that allows to define and track error types.
Why is the default Promise definition type bad?
An asynchronous code may throw errors but the standard type of the Promise cannot tell you which errors you can handle in the catch method. Of course, you can define the error type explicitly, but you should know what en error can be at the time. It may be a hard task, especially if you are chaining a lot of promises and each of them may throw an error.
Installation
npm i @halo-lab/futureAPI
- Overview
- Types
of/Future.offail/Future.failmake/Future.makespawn/Future.spawnisThenable/Future.ismerge/Future.mergesettle/Future.settlefirst/Future.firstoneOf/Future.oneOfmap/Future.mapmapErr/Future.mapErrrecover/Future.recoverafter/Future.after
Usage
This package defines Future/FutureLike types which you can use instead of the Promise/PromiseLike. These types are interchangeable.
import { Future } from "@halo-lab/future";
const future: Future<number, Error> = new Promise((resolve, reject) => {
const randomNumber = Math.random();
if (randomNumber > 0.5) resolve(randomNumber);
else reject(new Error("Random number is less than 0.5"));
});
const promise: Promise<number> = future;By using the Future you can describe what errors a promise can be rejected with and TypeScript will help you remember and exhaustively handle them later.
// using the example above
const newFuture: Future<string[], never> = future.then(
(number) => {
/* do something useful */
return ["foo"]; /* some result */
},
(error /* Error */) => {
/* report that there is a problem and fix it */
return [];
}
);Unfortunately,
awaited future inside thetry/catchblock cannot populate an error type to thecatchblock, because TypeScript doesn't allow it (even explicitly). Though you can refer to the future type inside thetryblock and easily get what errors are expected to be thrown.try { const value: string[] = await newFuture; } catch (error) { /* error is not typed as never but any or unknown depending on your tsconfig */ }
This package defines and exports some functions that make Future creation and managing easier because default Promise typings are plain and don't pay any attention to the error types. These functions are exported separately and in a namespace (as a default export) for convenience.
Types
The Future namespace defines also aliases for the Future type: Self and for the FutureLike type: Like.
import Future from "@halo-lab/future";
function one(): Future.Self<1, never> {
return Future.of(1);
}
const numberOne: Future.Like<1, never> = one();Besides these types the library exports:
NonThenable/Future.Not- a type that extracts thenable types from the type argument.
type A = Future.Not<number>; // -> number
type B = Future.Not<PromiseLike<string>>; // -> neverAwaitedError/Future.Left- extracts an error type from theFutureLike. If a type parameter isn't thenable, it returnsnever.
type A = Future.Left<number>; // -> never
type B = Future.Left<PromiseLike<string>>; // -> unknown
type C = Future.Left<Future.Like<string, number>>; // -> numberFuture.Right- an alias to the nativeAwaitedtype.
type A = Future.Right<number>; // -> number
type B = Future.Right<PromiseLike<string>>; // -> string
type C = Future.Right<Future.Like<string, number>>; // -> stringof/Future.of
Wraps a value with a Future and immediately resolves it. If the value is another Future, the latter isn't wrapped.
const wrappedNumber: Future.Self<10, never> = Future.of(10);
const duplicatedWrappedNumber: Future.Self<10, never> =
Future.of(wrappedNumber);
// The Future created from the Promise always has an `unknown`
// error type because it is really unknown unless the user knows it
// and provides the type manually.
const fromPromise: Future.Self<string, unknown> = Future.of(
Promise.resolve("foo")
);
// If the value is rejected Future or Promise, the resulting Future
// also has the rejected state.
const failedFuture: Future<never, string> = Future.of(
Promise.reject("A very helpful message")
);fail/Future.fail
Wraps a value with a Future and immediately rejects it. If the value is another Future, it will be awaited and a new Future will be rejected with either value.
const failedFuture: Future.Self<never, "error"> = Future.fail("error");
const failedPromise: Future.Self<never, number> = Future.fail(
Promise.resolve(7)
);make/Future.make
Creates a Future with an executor callback. The same as the Promise constructor.
const future: Future<number, string> = Future.make((ok, err) => {
doAsyncJob((error, result) => (error ? err(error) : ok(result)));
});spawn/Future.spawn
Creates a Future from a callback's result. If the callback throws an error, the Future will be rejected. If the callback returns another Future it will be returned as is.
function calculateFibonacciNumber(position: number): number {
// ...
}
const future: Future.Self<number, never> = Future.spawn(() => {
return calculateFibonacciNumber(57);
});
// There is no way to mark a function in TypeScript that can
// throw an error, so you have to describe the error type that
// manually. Otherwise, it will be `never`.
const trickyFuture: Future.Self<never, Error> = Future.spawn(() => {
throw new Error("an error is thrown");
});You can pass arguments into the callback by providing them after it.
const future: Future.Self<number, never> = Future.spawn(
(first, second) => {
return first + second;
},
[34, 97]
);isThenable/Future.is
Checks if a value is a thenable object.
Future.is(Future.of(1)); // -> true
Future.is(Promise.resolve("foo")); // -> true
Future.is({
then(fulfill) {
return Future.of(fulfill(Math.random()));
},
}); // -> true
Future.is(3); // -> falsemerge/Future.merge
Combines multiple Futures together waiting for all to complete or first to reject. Behaves as the Promise.all. Accepts a variable number of arguments or a single argument that should be Iterable or ArrayLike.
const result: Future.Self<readonly [number, string], boolean | string> =
Future.merge(
Future.spawn<number, boolean>(() => mayThrowABoolean()),
Future.spawn<string, string>(() => mayThrowAString())
);
const combined: Future.Self<readonly [1, 2], never> = Future.merge([
Future.of(1),
Future.of(2),
]);settle/Future.settle
Combines multiple Futures together waiting for all to complete. Behaves as the Promise.allSettled. Accepts a variable number of arguments or a single argument that should be Iterable or ArrayLike. Promise's values are wrapped with the special Result object. It is a plain object with either ok property or err.
const future: Future.Self<
readonly [Result<1, never>, Result<never, "bar">],
never
> = Future.settle(Future.ok(1), Future.fail("bar"));first/Future.first
Waits for the first Future to fulfill either successfuly or as a failure. Behaves as the Promise.race. Accepts a variable number of arguments or a single argument that should be Iterable or ArrayLike.
const future: Future.Self<1 | "foo", string | boolean> = Future.first(
Future.make<1, string>(
(ok, err) =>
setTimeout(() => {
Math.random() > 0.5
? ok(1)
: err("numbers greater than 0.5 are not acceptable");
}),
100
),
Future.spawn<"foo", boolean>(() => {
if (Math.random() > 0.5) return "foo";
else throw true;
})
);oneOf/Future.oneOf
Waits for the first Future to fulfill or all Futures to reject (array of errors is returned). Behaves as the Promise.any. Accepts a variable number of arguments or a single argument that should be Iterable or ArrayLike.
const future: Future.Self<1 | "foo", readonly [string, boolean]> = Future.oneOf(
Future.make<1, string>(
(ok, err) =>
setTimeout(() => {
Math.random() > 0.5
? ok(1)
: err("numbers greater than 0.5 are not acceptable");
}),
100
),
Future.spawn<"foo", boolean>(() => {
if (Math.random() > 0.5) return "foo";
else throw true;
})
);map/Future.map
Transforms a resolved value of the Future and returns another Future. It's a functional way to call onfulfilled callback of then method. The function has curried and uncurried forms.
const future: Future.Self<1, never> = Future.of(1);
const anotherFuture: Future.Self<number, never> = Future.map(
future,
(num) => num + 1
);
const multiplyByTen: <A>(
future: Future.Like<number, A>
) => Future.Self<number, A> = Future.map((num) => num * 10);
const multipliedFuture: Future.Self<number, never> = multiplyByTen(future);Callback is called only if the future is resolved. Otherwise it is returned as is.
mapErr/Future.mapErr
Transforms a rejected value of the Future into another rejected value and returns a rejected Future. The function has curried and uncurried forms.
const future: Future.Self<never, 1> = Future.fail(1);
const anotherFuture: Future.Self<never, number> = Future.mapErr(
future,
(num) => num + 1
);
const multiplyByTen: <A>(
future: Future.Like<A, number>
) => Future.Self<A, number> = Future.mapErr((num) => num * 10);
const multipliedFuture: Future.Self<never, number> = multiplyByTen(future);Callback is called only if the future is rejected. Otherwise it is returned as is.
recover/Future.recover
Transforms a rejected value of the Future into a resolved value and returns another Future. It's a functional way to call onrejected callback of the then method or the catch method. The function has curried and uncurried forms.
const future: Future.Self<OkResponse, ErrResponse> = fetch(
"/api/v3/endpoint"
).then((response) =>
response.ok ? response.json() : Future.fail(response.json())
);
// 1.
const futureWithDefaultResponse: Future.Self<OkResponse, never> =
Future.recover(future, (errResponse) =>
createDefaultResponseFrom(errResponse)
);
// 2.
const repairResponse: (
future: Future.Like<OkResponse, ErrResponse>
) => Future.Self<OkResponse, never> = Future.recover((errResponse) =>
createDefaultResponseFrom(errResponse)
);
const repairedResponse: Future.Self<OkResponse, never> = repairResponse(future);Callback is called only if the future is rejected. Otherwise it is returned as is.
after/Future.after
Registers a callback to be called after the Future fulfills either way. It's a functional way to call the finally method. The function has curried and uncurried forms.
const future: Future.Self<OkResponse, ErrResponse> = fetch(
"/api/v3/endpoint"
).then((response) =>
response.ok ? response.json() : Future.fail(response.json())
);
// 1.
const sameFuture: Future.Self<OkResponse, ErrResponse> = Future.after(
future,
() => doSomeSideEffect()
);
// 2.
const cleanupAfterJob: <OkResponse, ErrResponse>(
future: Future.Like<OkResponse, ErrResponse>
) => Future.Self<OkResponse, ErrResponse> = Future.after(() => doSomeCleanup());
const sameFutureAfterCleanup: Future.Self<OkResponse, ErrResponse> =
cleanupAfterJob(future);If a callback throws an error or returns a rejected Future the error is propagated into the resulting Future.
const future: Future.Self<never, string | boolean> = Future.after(
Future.fail("foo"),
() => Future.fail(false)
);Word from author
Have fun ✌️
