├── .github ├── CODEOWNERS └── workflows │ ├── ci.yml │ └── release.yml ├── .tool-versions ├── .gitignore ├── lib ├── async │ ├── mod.ts │ ├── operators_test.ts │ ├── tasks.ts │ ├── _internal.ts │ ├── operators.ts │ ├── tasks_test.ts │ ├── task.ts │ └── task_test.ts ├── core │ ├── mod.ts │ ├── assert.ts │ ├── assert_test.ts │ ├── errors_test.ts │ ├── errors.ts │ ├── type_utils.ts │ └── option_type_test.ts └── adapters │ └── web │ └── fetch │ ├── mod.ts │ └── mod_test.ts ├── mod.ts ├── dev_deps.ts ├── deno.jsonc ├── scripts ├── build_helpers.ts └── build_npm.ts ├── bench ├── sync_propagation_bench.ts ├── async_propagation_bench.ts ├── micro_bench.ts ├── sync_composition_bench.ts └── async_composition_bench.ts ├── LICENSE.md ├── CONTRIBUTING.md └── README.md /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | @realpha 2 | -------------------------------------------------------------------------------- /.tool-versions: -------------------------------------------------------------------------------- 1 | deno 1.41.3 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | npm/ 2 | cov/ 3 | **.lock 4 | **.lcov 5 | -------------------------------------------------------------------------------- /lib/async/mod.ts: -------------------------------------------------------------------------------- 1 | export * from "./task.ts"; 2 | export * as Tasks from "./tasks.ts"; 3 | -------------------------------------------------------------------------------- /mod.ts: -------------------------------------------------------------------------------- 1 | export * from "./lib/core/mod.ts"; 2 | export * from "./lib/async/mod.ts"; 3 | export * from "./lib/adapters/web/fetch/mod.ts"; 4 | -------------------------------------------------------------------------------- /dev_deps.ts: -------------------------------------------------------------------------------- 1 | export * from "https://deno.land/std@0.206.0/assert/mod.ts"; 2 | export * from "https://deno.land/std@0.206.0/testing/types.ts"; 3 | export { build, emptyDir } from "https://deno.land/x/dnt@0.38.1/mod.ts"; 4 | export * as semver from "https://deno.land/std@0.206.0/semver/mod.ts"; 5 | -------------------------------------------------------------------------------- /deno.jsonc: -------------------------------------------------------------------------------- 1 | { 2 | "fmt": { 3 | "exclude": ["cov/"] 4 | }, 5 | "tasks": { 6 | "build:npm": "deno run -A ./scripts/build_npm.ts", 7 | "build:cov": "deno coverage --lcov --output=./cov.lcov ./cov/", 8 | "test:ci": "deno test -A --doc --coverage=./cov", 9 | "test:cov": "deno test -A --coverage=./cov", 10 | "test": "deno test -A --doc" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /lib/core/mod.ts: -------------------------------------------------------------------------------- 1 | export { None, Option, Options, Some } from "./option.ts"; 2 | export { asInfallible, Err, Ok, Result, Results } from "./result.ts"; 3 | export { isEitherwayPanic, Panic, panic, unsafeCastTo } from "./errors.ts"; 4 | export type { IOption } from "./option.ts"; 5 | export type { IResult } from "./result.ts"; 6 | export type { 7 | Empty, 8 | Fallible, 9 | Falsy, 10 | HasToJSON, 11 | Infallible, 12 | JsonRepr, 13 | NonNullish, 14 | Nullish, 15 | StringRepr, 16 | Truthy, 17 | ValueRepr, 18 | } from "./type_utils.ts"; 19 | -------------------------------------------------------------------------------- /lib/core/assert.ts: -------------------------------------------------------------------------------- 1 | import { Panic } from "./errors.ts"; 2 | // Slightly modified from the Deno standard library https://github.com/denoland/deno_std/blob/main/assert/assert.ts 3 | export class AssertionError extends TypeError { 4 | name = "AssertionError"; 5 | constructor(message: string, options?: { cause?: unknown }) { 6 | super(message, options); 7 | } 8 | } 9 | 10 | // Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. 11 | // These are copied verbatim from the Deno standard library https://github.com/denoland/deno_std/blob/main/assert/assert.ts 12 | export function assert(expr: unknown, msg = ""): asserts expr { 13 | if (!expr) { 14 | throw new AssertionError(msg); 15 | } 16 | } 17 | 18 | export function assertNotNullish(value: T): asserts value is NonNullable { 19 | if (value == null) { 20 | Panic.causedBy(value, "Expected non-nullish value"); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: ["main"] 6 | pull_request: 7 | branches: ["main"] 8 | 9 | permissions: 10 | contents: read 11 | 12 | jobs: 13 | check: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - name: Setup repo 17 | uses: actions/checkout@v3 18 | 19 | - name: Setup deno 20 | uses: denoland/setup-deno@ef28d469f16304cf29da2bd31413743cc0c768e7 # v1.1.4 21 | with: 22 | deno-version: v1.41.3 23 | 24 | - name: Check fmt 25 | run: deno fmt --check 26 | 27 | - name: Run linter 28 | run: deno lint 29 | 30 | - name: Run tests & generate coverage 31 | run: deno task test:ci 32 | 33 | - name: Generate coverage report 34 | run: deno task build:cov 35 | 36 | - name: Upload coverage report 37 | uses: paambaati/codeclimate-action@v5.0.0 38 | env: 39 | CC_TEST_REPORTER_ID: ${{ secrets.CODECLIMATE_REPORTER_ID }} 40 | with: 41 | coverageLocations: ${{github.workspace}}/*.lcov:lcov 42 | -------------------------------------------------------------------------------- /scripts/build_helpers.ts: -------------------------------------------------------------------------------- 1 | import { Option, Result, Task } from "../mod.ts"; 2 | import { emptyDir, semver } from "../dev_deps.ts"; 3 | 4 | export type SemVer = semver.SemVer; 5 | export const ScriptErrors = { 6 | NoVersionProvided: TypeError("Expected version specifier, received none"), 7 | CouldNotPrepareDir: (e: unknown) => 8 | Error(`Could not prepare directory`, { cause: e }), 9 | CouldNotCreateFile: (e: unknown) => 10 | Error("Could not create file", { cause: e }), 11 | BuildFailed: (e: unknown) => Error(`Build failed`, { cause: e }), 12 | } as const; 13 | 14 | export const tryParse = Result.liftFallible( 15 | semver.parse, 16 | (e: unknown) => e as TypeError, 17 | ); 18 | 19 | /** 20 | * {@linkcode emptyDir} 21 | */ 22 | export const dirIsEmpty = Task.liftFallible( 23 | emptyDir, 24 | ScriptErrors.CouldNotPrepareDir, 25 | ); 26 | 27 | export function parseVersion(): Result { 28 | return Option.fromCoercible(Deno.args[0]) 29 | .okOr(ScriptErrors.NoVersionProvided) 30 | .inspect((v) => console.log(`Using version specifier: ${v}`)) 31 | .andThen(tryParse); 32 | } 33 | -------------------------------------------------------------------------------- /lib/core/assert_test.ts: -------------------------------------------------------------------------------- 1 | import { 2 | assertInstanceOf, 3 | assertStrictEquals, 4 | assertThrows, 5 | } from "../../dev_deps.ts"; 6 | import { assert, AssertionError } from "./assert.ts"; 7 | 8 | Deno.test("eitherway::core::assert", async (t) => { 9 | await t.step("AssertionError -> Is a subclass of TypeError", () => { 10 | const err = new AssertionError("test"); 11 | 12 | assertInstanceOf(err, Error); 13 | assertInstanceOf(err, TypeError); 14 | assertInstanceOf(err, AssertionError); 15 | assertStrictEquals(err.constructor, AssertionError); 16 | assertStrictEquals(err.name, "AssertionError"); 17 | assertStrictEquals(err.message, "test"); 18 | }); 19 | 20 | await t.step("AssertionError -> can be constructed with a cause", () => { 21 | const err = new AssertionError("test", { cause: new Error("boom") }); 22 | 23 | assertInstanceOf(err.cause, Error); 24 | assertStrictEquals(err.cause.message, "boom"); 25 | }); 26 | 27 | await t.step("assert() -> panics if expression evaluates to false", () => { 28 | assertThrows(() => { 29 | assert(false); 30 | }); 31 | }); 32 | }); 33 | -------------------------------------------------------------------------------- /bench/sync_propagation_bench.ts: -------------------------------------------------------------------------------- 1 | //deno-lint-ignore-file 2 | import { Err } from "../lib/core/mod.ts"; 3 | 4 | Deno.bench({ 5 | name: "Sync Exception Propagation", 6 | group: "Sync::Propagation", 7 | fn: () => { 8 | try { 9 | Exceptions.rethrow(); 10 | } catch (e) { 11 | } 12 | }, 13 | }); 14 | 15 | Deno.bench({ 16 | name: "Result Error Propagation", 17 | group: "Sync::Propagation", 18 | baseline: true, 19 | fn: () => { 20 | Errors.linearReturn(); 21 | }, 22 | }); 23 | 24 | export namespace Exceptions { 25 | function fail() { 26 | throw TypeError("Fail!"); 27 | } 28 | function propagate() { 29 | try { 30 | return fail(); 31 | } catch (e) { 32 | throw e; 33 | } 34 | } 35 | export function rethrow() { 36 | try { 37 | return propagate(); 38 | } catch (e) { 39 | throw e; 40 | } 41 | } 42 | } 43 | 44 | export namespace Errors { 45 | function fail() { 46 | return Err(TypeError("Fail!")); 47 | } 48 | function propagate() { 49 | return fail(); 50 | } 51 | export function linearReturn() { 52 | return propagate(); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2023 realpha <0xrealpha@proton.me> 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /bench/async_propagation_bench.ts: -------------------------------------------------------------------------------- 1 | //deno-lint-ignore-file 2 | import { Task } from "../lib/async/mod.ts"; 3 | 4 | function sleep(ms: number): Promise { 5 | return new Promise((resolve) => setTimeout(resolve, ms)); 6 | } 7 | 8 | Deno.bench({ 9 | name: "Async Exception Propagation", 10 | group: "Async::Propagation", 11 | fn: async () => { 12 | try { 13 | await AsyncExceptions.rethrow(); 14 | } catch (e) { 15 | } 16 | }, 17 | }); 18 | 19 | Deno.bench({ 20 | name: "Task Error Propagation", 21 | group: "Async::Propagation", 22 | baseline: true, 23 | fn: async () => { 24 | await TaskErrors.linearReturn(); 25 | }, 26 | }); 27 | 28 | export namespace AsyncExceptions { 29 | async function fail() { 30 | await sleep(1); 31 | throw TypeError("Fail!"); 32 | } 33 | async function propagate() { 34 | try { 35 | await sleep(1); 36 | return await fail(); 37 | } catch (e) { 38 | throw e; 39 | } 40 | } 41 | export async function rethrow() { 42 | try { 43 | await sleep(1); 44 | return await propagate(); 45 | } catch (e) { 46 | throw e; 47 | } 48 | } 49 | } 50 | 51 | export namespace TaskErrors { 52 | async function fail() { 53 | await sleep(1); 54 | return Task.fail(TypeError("Fail!")); 55 | } 56 | async function propagate() { 57 | await sleep(1); 58 | return await fail(); 59 | } 60 | export async function linearReturn() { 61 | await sleep(1); 62 | return await propagate(); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /lib/core/errors_test.ts: -------------------------------------------------------------------------------- 1 | import { 2 | assertInstanceOf, 3 | assertStrictEquals, 4 | assertThrows, 5 | assertType, 6 | IsExact, 7 | } from "../../dev_deps.ts"; 8 | import { isEitherwayPanic, Panic, panic } from "./errors.ts"; 9 | 10 | Deno.test("eitherway::core::errors", async (t) => { 11 | await t.step("Panic -> Contructors", async (t) => { 12 | await t.step("new() -> creates a generic instance", () => { 13 | const cause = new Error("boom"); 14 | const pnc = new Panic(cause, "Runtime exception"); 15 | 16 | assertType>>(true); 17 | assertInstanceOf(pnc, Error); 18 | assertInstanceOf(pnc, Panic); 19 | assertStrictEquals(pnc.constructor, Panic); 20 | assertStrictEquals(pnc.name, "eitherway::Panic"); 21 | assertStrictEquals(pnc.message, "Runtime exception"); 22 | assertStrictEquals(pnc.cause, cause); 23 | }); 24 | await t.step(".causedBy() -> creates a generic instance", () => { 25 | const cause = Error("boom"); 26 | const pnc = Panic.causedBy(cause, "Runtime exception"); 27 | 28 | assertType>>(true); 29 | assertInstanceOf(pnc, Error); 30 | assertInstanceOf(pnc, Panic); 31 | assertStrictEquals(pnc.constructor, Panic); 32 | assertStrictEquals(pnc.name, "eitherway::Panic"); 33 | assertStrictEquals(pnc.message, "Runtime exception"); 34 | assertStrictEquals(pnc.cause, cause); 35 | }); 36 | }); 37 | 38 | await t.step( 39 | "isEitherwayPanic() -> narrows an unknown value to a Panic", 40 | () => { 41 | const err = Error("boom"); 42 | const pnc = Panic.causedBy(null, "Expected non-nullish value"); 43 | 44 | assertStrictEquals(isEitherwayPanic(pnc), true); 45 | assertStrictEquals(isEitherwayPanic(err), false); 46 | assertStrictEquals(isEitherwayPanic(null), false); 47 | assertStrictEquals(isEitherwayPanic(true), false); 48 | }, 49 | ); 50 | 51 | await t.step("panic() -> throws the provided error", () => { 52 | const err = TypeError("boom"); 53 | 54 | assertThrows(() => panic(err), TypeError, "boom"); 55 | }); 56 | 57 | await t.step( 58 | "panic() -> throws a Panic if err is not a subtype of native Error", 59 | () => { 60 | const rec = { 61 | name: "ErrorLike", 62 | message: "boom", 63 | cause: null, 64 | stack: "ErrorLike: boom\n", 65 | }; 66 | 67 | assertThrows(() => panic(rec), Panic, "unknown error"); 68 | }, 69 | ); 70 | 71 | await t.step("panic() -> throws a Panic if err is nullish", () => { 72 | assertThrows(() => panic(), Panic, "unknown error"); 73 | }); 74 | }); 75 | -------------------------------------------------------------------------------- /scripts/build_npm.ts: -------------------------------------------------------------------------------- 1 | import { Err, Ok, Result, Task } from "../mod.ts"; 2 | import { 3 | dirIsEmpty, 4 | parseVersion, 5 | ScriptErrors, 6 | SemVer, 7 | } from "./build_helpers.ts"; 8 | import { build, semver } from "../dev_deps.ts"; 9 | 10 | const PKG_NAME = "eitherway"; 11 | const ENTRY_POINT = "./mod.ts"; 12 | const OUT_DIR = "./npm"; 13 | const LICENSE = "./LICENSE.md"; 14 | const README = "./README.md"; 15 | const GIT_URL = "git+https://github.com/realpha/eitherway.git"; 16 | const ISSUE_URL = "https://github.com/realpha/eitherway/issues"; 17 | 18 | async function buildPackage(v: SemVer): Promise> { 19 | try { 20 | await build({ 21 | entryPoints: [ENTRY_POINT], 22 | outDir: OUT_DIR, 23 | typeCheck: "both", 24 | declaration: "separate", 25 | test: false, 26 | shims: { 27 | deno: false, 28 | }, 29 | package: { 30 | name: PKG_NAME, 31 | version: semver.format(v), 32 | description: 33 | "Safe abstractions for fallible flows inspired by F# and Rust", 34 | license: "MIT", 35 | author: "realpha <0xrealpha@proton.me>", 36 | engines: { 37 | "node": ">=17.0.0", //needed for structuredClone 38 | }, 39 | repository: { 40 | type: "git", 41 | url: GIT_URL, 42 | }, 43 | bugs: { 44 | url: ISSUE_URL, 45 | }, 46 | keywords: [ 47 | "async", 48 | "either", 49 | "error", 50 | "error-handling", 51 | "fsharp", 52 | "fallible", 53 | "functional", 54 | "maybe", 55 | "monad", 56 | "option", 57 | "result", 58 | "rust", 59 | "task", 60 | "typescript", 61 | ], 62 | }, 63 | compilerOptions: { 64 | lib: ["DOM", "ES2022"], //needed for structuredClone 65 | target: "ES2022", 66 | }, 67 | postBuild() { 68 | Deno.copyFileSync(LICENSE, `${OUT_DIR}/LICENSE.md`); 69 | Deno.copyFileSync(README, `${OUT_DIR}/README.md`); 70 | Deno.removeSync(`${OUT_DIR}/src`, { recursive: true }); 71 | }, 72 | }); 73 | return Ok(undefined); 74 | } catch (e: unknown) { 75 | return Err(ScriptErrors.BuildFailed(e)); 76 | } 77 | } 78 | 79 | function main() { 80 | return parseVersion() 81 | .into(Task.of) 82 | .andEnsure(() => dirIsEmpty(OUT_DIR)) 83 | .andThen(buildPackage); 84 | } 85 | 86 | main().then((res) => { 87 | const code = res 88 | .inspect(() => console.log("Build succeeded!")) 89 | .inspectErr(console.error) 90 | .mapOr(() => 0, 1) 91 | .unwrap(); 92 | 93 | Deno.exit(code); 94 | }); 95 | -------------------------------------------------------------------------------- /lib/async/operators_test.ts: -------------------------------------------------------------------------------- 1 | //deno-lint-ignore-file require-await 2 | import { Err, Ok, Result } from "../core/mod.ts"; 3 | import { Task } from "./task.ts"; 4 | import * as Operators from "./operators.ts"; 5 | import { assertStrictEquals } from "../../dev_deps.ts"; 6 | 7 | Deno.test("eitherway::Task::Operators", async (t) => { 8 | await t.step("Map Operators", async (t) => { 9 | await t.step( 10 | "map() -> returns Promise with applied mapFn", 11 | async () => { 12 | const p: Promise> = Promise.resolve(Ok(41)); 13 | 14 | const mapped = p.then(Operators.map((x) => x + 1)); 15 | const res = await mapped; 16 | 17 | assertStrictEquals(res.isOk(), true); 18 | assertStrictEquals(res.unwrap(), 42); 19 | }, 20 | ); 21 | 22 | await t.step( 23 | "mapErr() -> returns Promise with applied mapFn to err", 24 | async () => { 25 | const e = TypeError("Cannot do that"); 26 | const p: Promise> = Promise.resolve(Err(e)); 27 | 28 | const mapped = p.then( 29 | Operators.mapErr((e) => TypeError("Received error", { cause: e })), 30 | ); 31 | const res = await mapped; 32 | 33 | assertStrictEquals(res.isErr(), true); 34 | //@ts-expect-error The assertion above doesn't narrow the type 35 | assertStrictEquals(res.unwrap().cause, e); 36 | }, 37 | ); 38 | 39 | await t.step( 40 | "andThen() -> returns Promise with applied fn to value", 41 | async () => { 42 | const p: Promise> = Promise.resolve( 43 | Ok("1"), 44 | ); 45 | const safeParse = async function ( 46 | str: string, 47 | ): Promise> { 48 | const n = Number.parseInt(str); 49 | 50 | return Number.isNaN(n) ? Err(TypeError("Cannot parse")) : Ok(n); 51 | }; 52 | 53 | const chained = p.then(Operators.andThen(safeParse)); 54 | const res = await chained; 55 | 56 | assertStrictEquals(res.isOk(), true); 57 | assertStrictEquals(res.unwrap(), 1); 58 | }, 59 | ); 60 | 61 | await t.step( 62 | "orElse() -> returns a Promise with applied fn to err", 63 | async () => { 64 | const p: Promise> = Promise.resolve(Err( 65 | Error("Received error", { cause: TypeError("Cannot do that") }), 66 | )); 67 | const rehydrate = function (err: unknown) { 68 | if (!(err instanceof Error)) { 69 | return Task.fail(RangeError("Cannot rehydrate")); 70 | } 71 | return Task.succeed(0); 72 | }; 73 | 74 | const chained = p.then(Operators.orElse(rehydrate)); 75 | const res = await chained; 76 | 77 | assertStrictEquals(res.isOk(), true); 78 | assertStrictEquals(res.unwrap(), 0); 79 | }, 80 | ); 81 | }); 82 | }); 83 | -------------------------------------------------------------------------------- /bench/micro_bench.ts: -------------------------------------------------------------------------------- 1 | //deno-lint-ignore-file 2 | import { asInfallible, Err, Ok, Result } from "../lib/core/result.ts"; 3 | import { Task } from "../lib/async/task.ts"; 4 | import { Option } from "../lib/core/option.ts"; 5 | 6 | const ERR = Err("foo"); 7 | const OK = Ok("foo"); 8 | 9 | async function produceRes(): Promise> { 10 | return Ok("foo"); 11 | } 12 | 13 | async function produceValue(): Promise { 14 | return "foo"; 15 | } 16 | 17 | Deno.bench({ 18 | name: "Promise.resolve(Ok)", 19 | group: "Micro::Async::Construction", 20 | fn: () => { 21 | const res = Promise.resolve(OK); 22 | }, 23 | }); 24 | 25 | Deno.bench({ 26 | name: "new Promise(Ok)", 27 | group: "Micro::Async::Construction", 28 | baseline: true, 29 | fn: () => { 30 | const res = new Promise((resolve) => resolve(OK)); 31 | }, 32 | }); 33 | 34 | Deno.bench({ 35 | name: "Task.succeed", 36 | group: "Micro::Async::Construction", 37 | fn: () => { 38 | const res = Task.succeed("foo"); 39 | }, 40 | }); 41 | 42 | Deno.bench({ 43 | name: "Task.of(Ok)", 44 | group: "Micro::Async::Construction", 45 | fn: () => { 46 | const res = Task.of(OK); 47 | }, 48 | }); 49 | 50 | Deno.bench({ 51 | name: "Promise.resolve(Err)", 52 | group: "Micro::Async::Construction", 53 | fn: () => { 54 | const res = Promise.resolve(ERR); 55 | }, 56 | }); 57 | 58 | Deno.bench({ 59 | name: "new Promise(Err)", 60 | group: "Micro::Async::Construction", 61 | baseline: true, 62 | fn: () => { 63 | const res = new Promise((resolve) => resolve(ERR)); 64 | }, 65 | }); 66 | 67 | Deno.bench({ 68 | name: "Task.fail", 69 | group: "Micro::Async::Construction", 70 | fn: () => { 71 | const res = Task.fail("foo"); 72 | }, 73 | }); 74 | 75 | Deno.bench({ 76 | name: "Task.of(Err)", 77 | group: "Micro::Async::Construction", 78 | fn: () => { 79 | const res = Task.of(ERR); 80 | }, 81 | }); 82 | 83 | Deno.bench({ 84 | name: "Task.of(Promise)", 85 | group: "Micro::Async::Construction", 86 | fn: () => { 87 | const res = Task.of(produceRes()); 88 | }, 89 | }); 90 | 91 | Deno.bench({ 92 | name: "Task.fromFallible(() => Promise)", 93 | group: "Micro::Async::Construction", 94 | fn: () => { 95 | const res = Task.fromFallible(produceValue, asInfallible); 96 | }, 97 | }); 98 | 99 | Deno.bench({ 100 | name: "AsyncFn() => Promise)", 101 | group: "Micro::Async::Construction", 102 | fn: () => { 103 | const res = produceRes(); 104 | }, 105 | }); 106 | 107 | Deno.bench({ 108 | name: "Ok", 109 | group: "Micro::Construction", 110 | fn: () => { 111 | const res = Ok("foo"); 112 | }, 113 | }); 114 | 115 | Deno.bench({ 116 | name: "Err", 117 | group: "Micro::Construction", 118 | fn: () => { 119 | const res = Err("foo"); 120 | }, 121 | }); 122 | 123 | Deno.bench({ 124 | name: "Option", 125 | group: "Micro::Construction", 126 | fn: () => { 127 | const res = Option("foo"); 128 | }, 129 | }); 130 | -------------------------------------------------------------------------------- /lib/core/errors.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * The canonical runtime exception type used internally 3 | * 4 | * @internal 5 | * @category Core::Errors 6 | */ 7 | export class Panic extends Error { 8 | name = "eitherway::Panic"; 9 | readonly cause?: E; 10 | constructor(cause?: E, msg?: string) { 11 | super(msg, { cause }); 12 | this.cause = cause; 13 | } 14 | 15 | static causedBy(cause?: E, msg = "Panic"): Panic { 16 | return new Panic(cause, msg); 17 | } 18 | } 19 | 20 | /** 21 | * Use this to either throw `` directly or wrap it in a `Panic` 22 | * if `` is not a subtype of the native `Error` type 23 | * 24 | * This can be useful in situations where: 25 | * - the encountered error is truly unrecoverable 26 | * - error handling is unfeasible 27 | * - this behaviour is required per some contract (e.g. middlewares) 28 | * - you want to emulate the behavior of `.unwrap()` from other libraries or 29 | * languages 30 | * 31 | * @throws {E|Panic} 32 | * 33 | * @category Core::Errors 34 | * 35 | * @example 36 | * ```typescript 37 | * import { Result, Option } from "https://deno.land/x/eitherway/mod.ts"; 38 | * import { panic } from "./mod.ts" 39 | * 40 | * function parseHex(hex: string): Result { 41 | * const candidate = Number.parseInt(hex, 16); 42 | * return Option.fromCoercible(candidate).okOrElse(() => TypeError()); 43 | * } 44 | * 45 | * const res = Result("0x10f2c").andThen(parseHex); 46 | * 47 | * const value: number = res.unwrapOrElse(panic); 48 | * ``` 49 | */ 50 | export function panic(err?: E): never { 51 | if (err instanceof Error) { 52 | throw err; 53 | } 54 | throw Panic.causedBy(err, "Panic caused by unknown error!"); 55 | } 56 | 57 | /** 58 | * Use this to narrow an `unknown` error to a `Panic` 59 | * 60 | * @category Core::Errors 61 | */ 62 | export function isEitherwayPanic(err: unknown): err is Panic { 63 | return err != null && typeof err === "object" && err.constructor === Panic; 64 | } 65 | 66 | /** 67 | * Use this to cast a `unknown` value to a known type. 68 | * 69 | * This is mostly useful when integrating external, fallible code, where one 70 | * ought to deal with thrown exceptions (and this type is in fact documented). 71 | * 72 | * Please note, that this is unsafe and only provided for prototyping purposes 73 | * and experimentation. 74 | * 75 | * @category Core::Errors 76 | * 77 | * @example Lifting a fallible function 78 | * ```typescript 79 | * import { Result } from "https://deno.land/x/eitherway/mod.ts"; 80 | * import { unsafeCastTo } from "./mod.ts"; 81 | * 82 | * function parseHex(hex: string): number { 83 | * const candidate = Number.parseInt(hex, 16); 84 | * 85 | * if (Number.isNaN(candidate)) throw TypeError(); 86 | * return candidate; 87 | * } 88 | * 89 | * const safeParseHex = Result.liftFallible( 90 | * parseHex, 91 | * unsafeCastTo, 92 | * ); 93 | * 94 | * const res = Result("0x10f2c").andThen(safeParseHex); 95 | * 96 | * const value: number = res.unwrapOr(0); 97 | * ``` 98 | */ 99 | export function unsafeCastTo(err: unknown): E { 100 | return err as E; 101 | } 102 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: release 2 | 3 | on: 4 | push: 5 | tags: 6 | - '[0-9]+.[0-9]+.[0-9]+' 7 | 8 | jobs: 9 | check: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Setup repo 13 | uses: actions/checkout@v3 14 | 15 | - name: Setup deno 16 | uses: denoland/setup-deno@ef28d469f16304cf29da2bd31413743cc0c768e7 # v1.1.4 17 | with: 18 | deno-version: v1.41.3 19 | 20 | - name: Check fmt 21 | run: deno fmt --check 22 | 23 | - name: Run linter 24 | run: deno lint 25 | 26 | - name: Run tests 27 | run: deno test -A 28 | 29 | build: 30 | runs-on: ubuntu-latest 31 | steps: 32 | - name: Setup repo 33 | uses: actions/checkout@v3 34 | 35 | - name: Setup deno 36 | uses: denoland/setup-deno@ef28d469f16304cf29da2bd31413743cc0c768e7 # v1.1.4 37 | with: 38 | deno-version: v1.41.3 39 | 40 | - name: Extract tag version 41 | if: startsWith(github.ref, 'refs/tags/') 42 | id: tag_version 43 | run: echo ::set-output name=TAG_VERSION::${GITHUB_REF/refs\/tags\//} 44 | 45 | - name: Build npm package 46 | run: deno task build:npm ${{steps.tag_version.outputs.TAG_VERSION}} 47 | 48 | - name: Chache package 49 | uses: actions/upload-artifact@v3 50 | with: 51 | name: npm-${{steps.tag_version.outputs.TAG_VERSION}} 52 | path: npm/ 53 | 54 | #publish-release 55 | #TODO: Setup changelog and extract changes to populate release notes 56 | 57 | publish-npm: 58 | runs-on: ubuntu-latest 59 | needs: [check, build] 60 | steps: 61 | - name: Setup repo 62 | uses: actions/checkout@v3 63 | 64 | - name: Setup Node 65 | uses: actions/setup-node@v4 66 | with: 67 | node-version: 'lts/Hydrogen' 68 | registry-url: 'https://registry.npmjs.org' 69 | 70 | - name: Extract tag version 71 | if: startsWith(github.ref, 'refs/tags/') 72 | id: tag_version 73 | run: echo ::set-output name=TAG_VERSION::${GITHUB_REF/refs\/tags\//} 74 | 75 | - name: Download cached package 76 | uses: actions/download-artifact@v3 77 | with: 78 | name: npm-${{steps.tag_version.outputs.TAG_VERSION}} 79 | path: npm 80 | 81 | - name: Publish 82 | env: 83 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 84 | working-directory: npm 85 | run: npm publish 86 | 87 | # publish-jsr: 88 | # runs-on: ubuntu-latest 89 | # needs: [check, build] 90 | # permissions: 91 | # contents: read 92 | # id-token: write 93 | # steps: 94 | # - name: Setup repo 95 | # uses: actions/checkout@v3 96 | # 97 | # - name: Setup deno 98 | # uses: denoland/setup-deno@ef28d469f16304cf29da2bd31413743cc0c768e7 # v1.1.4 99 | # with: 100 | # deno-version: v1.41.3 101 | # 102 | # - name: Extract tag version 103 | # if: startsWith(github.ref, 'refs/tags/') 104 | # id: tag_version 105 | # run: echo ::set-output name=TAG_VERSION::${GITHUB_REF/refs\/tags\//} 106 | # 107 | # - name: Build jsr manifest 108 | # run: deno task build:jsr ${{steps.tag_version.outputs.TAG_VERSION}} 109 | # 110 | # - name: Publish 111 | # run: deno publish 112 | 113 | -------------------------------------------------------------------------------- /lib/core/type_utils.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * =============== 3 | * TYPES 4 | * =============== 5 | */ 6 | 7 | /** 8 | * Representation of how type T will be passed on to searialization to 9 | * `JSON.stringify()` 10 | * 11 | * The square brackets are used to prevent distribution over unions where 12 | * never is erased anyway and we can actuall match never, which is 13 | * necessary for None 14 | * 15 | * Reference: 16 | * https://www.typescriptlang.org/docs/handbook/2/conditional-types.html#distributive-conditional-types 17 | */ 18 | export type JsonRepr = [T] extends { toJSON(): infer R } ? R 19 | : [T] extends [never] ? undefined 20 | : T; 21 | 22 | /** 23 | * String representation of type T, i.e. the return type of .toString() 24 | */ 25 | export type StringRepr = [T] extends { toString(): infer R } ? R 26 | : [T] extends [never] ? string 27 | : unknown; 28 | 29 | /** 30 | * Value representation of type T, i.e. the return type of .valueOf() 31 | */ 32 | export type ValueRepr = [T] extends { valueOf(): infer R } ? R 33 | : [T] extends [never] ? number 34 | : unknown; 35 | 36 | export type Nullish = null | undefined; 37 | /** 38 | * Ref: https://developer.mozilla.org/en-US/docs/Glossary/Falsy 39 | */ 40 | export type Falsy = Nullish | false | "" | 0 | -0 | 0n; 41 | export type Fallible = E extends Error ? E : never; 42 | 43 | export type Truthy = Exclude; 44 | export type NonNullish = Exclude; 45 | export type Infallible = Exclude>; 46 | export type HasToJSON = T extends { toJSON(): JsonRepr } ? T : never; 47 | 48 | export type NonReadonly = T extends Readonly ? U : T; 49 | 50 | /** 51 | * An artificial bottom type en lieu of unknown and nullish types 52 | * 53 | * This is used as a combatibility safe-guard for conversions between 54 | * `Result` and `Option`, where a value of type `Ok` 55 | * would evaluate to `None` when converted with `.ok()`. 56 | * 57 | * @example 58 | * ```typescript 59 | * import { assert } from "./assert.ts"; 60 | * import { Empty, Ok, Option, Result, Some } from "./mod.ts"; 61 | * 62 | * const voidResult: Result = Ok(undefined); 63 | * const emptyResult: Result = Ok.empty(); 64 | * 65 | * assert(voidResult.ok().isNone() === true); 66 | * assert(emptyResult.ok().isNone() === false); 67 | * ``` 68 | */ 69 | //deno-lint-ignore ban-types 70 | export type Empty = Readonly<{}>; 71 | export const EMPTY: Empty = Object.freeze(Object.create(null)); 72 | 73 | /** 74 | * =============== 75 | * TYPE PREDICATES 76 | * =============== 77 | */ 78 | 79 | export function isNotNullish(arg: T): arg is NonNullish { 80 | return arg != null; 81 | } 82 | 83 | export function isTruthy(arg: T): arg is Truthy { 84 | return Boolean(arg); 85 | } 86 | 87 | export function isInfallible(arg: T): arg is Infallible { 88 | if (arg == null || arg instanceof Error) return false; 89 | return true; 90 | } 91 | 92 | export function hasToJSON(arg: T): arg is HasToJSON { 93 | const method = "toJSON"; 94 | const target = Object(arg); 95 | return method in target && typeof target[method] === "function"; 96 | } 97 | 98 | export function isPrimitive(arg: unknown): arg is string | number | boolean { 99 | const type = typeof arg; 100 | return type === "string" || type === "number" || type === "boolean"; 101 | } 102 | -------------------------------------------------------------------------------- /bench/sync_composition_bench.ts: -------------------------------------------------------------------------------- 1 | //deno-lint-ignore-file 2 | import { Option, Result } from "../lib/core/mod.ts"; 3 | 4 | const INPUTS = [ 5 | undefined, 6 | "fortytwo", 7 | "lenghtySuperLenghtyGibberishString", 8 | "", 9 | "5", 10 | ]; 11 | 12 | Deno.bench({ 13 | name: "Sync Exceptions", 14 | group: "Sync::Composition", 15 | fn: () => { 16 | INPUTS.forEach((i) => { 17 | try { 18 | SyncExceptions.processString(i); 19 | } catch (e) { 20 | //nothing to do here 21 | } 22 | }); 23 | }, 24 | }); 25 | 26 | Deno.bench({ 27 | name: "Result Instance Composition", 28 | group: "Sync::Composition", 29 | baseline: true, 30 | fn: () => { 31 | INPUTS.forEach((i) => SyncResults.instanceComposition(i)); 32 | }, 33 | }); 34 | 35 | Deno.bench({ 36 | name: "Result Early Return", 37 | group: "Sync::Composition", 38 | fn: () => { 39 | INPUTS.forEach((i) => SyncResults.earlyReturn(i)); 40 | }, 41 | }); 42 | 43 | namespace SyncExceptions { 44 | function toUpperCase(input: string | undefined): string { 45 | if (typeof input === "undefined") { 46 | throw new TypeError("Input is undefined"); 47 | } 48 | return input.toUpperCase(); 49 | } 50 | 51 | function stringToLength(input: string): number { 52 | if (input.length === 0) { 53 | throw new TypeError("Input string is empty"); 54 | } 55 | return input.length; 56 | } 57 | 58 | function powerOfSelf(input: number): number { 59 | if ([Infinity, -Infinity, NaN, undefined].includes(input)) { 60 | throw new TypeError("Input is not a valid number"); 61 | } 62 | 63 | const res = Math.pow(input, input); 64 | 65 | if ([Infinity, -Infinity, NaN, undefined].includes(res)) { 66 | throw new TypeError("Cannot calculate result"); 67 | } 68 | 69 | return res; 70 | } 71 | 72 | export function processString(input: string | undefined): number { 73 | try { 74 | const upperCased = toUpperCase(input); 75 | const length = stringToLength(upperCased); 76 | return powerOfSelf(length); 77 | } catch (error) { 78 | throw error; 79 | } 80 | } 81 | } 82 | 83 | namespace SyncResults { 84 | function toUpperCase(input: string | undefined): Result { 85 | return Option(input) 86 | .okOrElse(() => TypeError("Input is undefined")) 87 | .map((str) => str.toUpperCase()); 88 | } 89 | 90 | function stringToLength(input: string): Result { 91 | return Option.fromCoercible(input) 92 | .okOrElse(() => TypeError("Input string is empty")) 93 | .map((str) => str.length); 94 | } 95 | 96 | function powerOfSelf(input: number): Result { 97 | return Option.fromCoercible(input) 98 | .okOrElse(() => TypeError("Input is not a valid number")) 99 | .andThen((n) => { 100 | return Option.fromCoercible(Math.pow(n, n)) 101 | .okOrElse(() => TypeError("Cannot calculate result")); 102 | }); 103 | } 104 | 105 | export function instanceComposition( 106 | input: string | undefined, 107 | ): Result { 108 | return toUpperCase(input) 109 | .andThen(stringToLength) 110 | .andThen(powerOfSelf); 111 | } 112 | 113 | export function earlyReturn( 114 | input: string | undefined, 115 | ): Result { 116 | const upperCased = toUpperCase(input); 117 | if (upperCased.isErr()) return upperCased; 118 | 119 | const length = stringToLength(upperCased.unwrap()); 120 | if (length.isErr()) return length; 121 | 122 | return powerOfSelf(length.unwrap()); 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /lib/adapters/web/fetch/mod.ts: -------------------------------------------------------------------------------- 1 | import { Task } from "../../../async/task.ts"; 2 | import { Err, Ok, Result } from "../../../core/result.ts"; 3 | 4 | export interface ResponseLike { 5 | ok: boolean; 6 | status: number; 7 | statusText: string; 8 | } 9 | 10 | export interface FailedRequestJson { 11 | status: number; 12 | statusText: string; 13 | } 14 | 15 | export class FailedRequest extends Error { 16 | name = "FailedRequest"; 17 | status: number; 18 | statusText: string; 19 | #response: R; 20 | constructor(res: R) { 21 | super(res.statusText); 22 | this.#response = res; 23 | this.status = res.status; 24 | this.statusText = res.statusText; 25 | } 26 | 27 | static from(res: R): FailedRequest { 28 | return new FailedRequest(res); 29 | } 30 | 31 | static with(status: number, statusText: string): FailedRequest { 32 | return new FailedRequest( 33 | new Response(null, { status, statusText }), 34 | ); 35 | } 36 | 37 | getResponse(): R { 38 | return this.#response; 39 | } 40 | 41 | toJSON(): FailedRequestJson { 42 | return { 43 | status: this.status, 44 | statusText: this.statusText, 45 | }; 46 | } 47 | } 48 | 49 | export class FetchException extends Error { 50 | name = "FetchException"; 51 | cause: Error | TypeError; 52 | constructor(cause: unknown) { 53 | super("Fetch paniced - operation aborted."); 54 | this.cause = cause instanceof Error 55 | ? cause 56 | : Error("Unknown exception", { cause }); 57 | } 58 | 59 | static from(cause: unknown): FetchException { 60 | return new FetchException(cause); 61 | } 62 | } 63 | 64 | export function isFetchException(err: unknown): err is FetchException { 65 | return err != null && typeof err === "object" && 66 | err.constructor === FetchException; 67 | } 68 | 69 | export function toFetchResult( 70 | res: R, 71 | ): Result> { 72 | if (res.ok) return Ok(res); 73 | return Err(FailedRequest.from(res)); 74 | } 75 | 76 | /** 77 | * Use this to lift a fetch-like function into a Task context. 78 | * 79 | * This is a constrained wrapper over `Task.liftFallible` and comes with a default 80 | * `ctorFn` and `errMapFn` for the 2nd and 3rd parameter respectively. 81 | * 82 | * @param fetchLike - Function to lift. Any function returning a `ResponseLike` object. 83 | * @param ctorFn - Result or Task constructor function. Use this to distinguish successful from failed requests. 84 | * @param errMapFn - Error map function. Maps any exception to a known error. 85 | * 86 | * @category Adapters 87 | * 88 | * @example Basic Usage 89 | * ```typescript 90 | * import { liftFetch } from "./mod.ts"; 91 | * 92 | * interface Company { 93 | * name: string; 94 | * } 95 | * 96 | * interface User { 97 | * id: number; 98 | * company: Company; 99 | * } 100 | * 101 | * const lifted = liftFetch(fetch); 102 | * const resourceUrl = "https://jsonplaceholder.typicode.com/users/1"; 103 | * 104 | * const result: Company = await lifted(resourceUrl) 105 | * .map(async (resp) => (await resp.json()) as User) // Lazy, use validation! 106 | * .inspect(console.log) 107 | * .inspectErr(console.error) // FailedRequest | FetchException 108 | * .mapOr(user => user.company, { name: "Acme Corp." }) 109 | * .unwrap(); 110 | * ``` 111 | * 112 | * @example With custom Response type 113 | * ```typescript 114 | * import { liftFetch } from "./mod.ts"; 115 | * 116 | * interface Company { 117 | * name: string; 118 | * } 119 | * 120 | * interface User { 121 | * id: number; 122 | * company: Company; 123 | * } 124 | * 125 | * interface UserResponse extends Response { 126 | * json(): Promise 127 | * } 128 | * 129 | * function fetchUserById(id: number): Promise { 130 | * return fetch(`https://jsonplaceholder.typicode.com/users/${id}`); 131 | * } 132 | * 133 | * const lifted = liftFetch(fetchUserById); 134 | * 135 | * const result: Company = await lifted(1) 136 | * .map((resp) => resp.json()) // inferred as User 137 | * .inspect(console.log) 138 | * .inspectErr(console.error) // FailedRequest | FetchException 139 | * .mapOr(user => user.company, { name: "Acme Corp." }) 140 | * .unwrap(); 141 | * ``` 142 | */ 143 | export function liftFetch< 144 | Args extends unknown[], 145 | R extends ResponseLike, 146 | T = R, 147 | E1 = FailedRequest, 148 | E2 = FetchException, 149 | >( 150 | fetchLike: (...args: Args) => R | PromiseLike, 151 | ctorFn?: (arg: R) => Result | PromiseLike>, 152 | errMapFn?: (cause: unknown) => E2, 153 | ): (...args: Args) => Task { 154 | return Task.liftFallible( 155 | fetchLike, 156 | (errMapFn ?? FetchException.from) as (cause: unknown) => E2, 157 | (ctorFn ?? toFetchResult) as (arg: R) => Result, 158 | ); 159 | } 160 | -------------------------------------------------------------------------------- /bench/async_composition_bench.ts: -------------------------------------------------------------------------------- 1 | //deno-lint-ignore-file 2 | import { Option, Result } from "../lib/core/mod.ts"; 3 | import { Task } from "../lib/async/task.ts"; 4 | import * as TaskOps from "../lib/async/operators.ts"; 5 | 6 | const INPUTS = [ 7 | "casio", 8 | undefined, 9 | "lenghtySuperLenghtyGibberishString", 10 | "", 11 | "5", 12 | undefined, 13 | "houseknifes", 14 | undefined, 15 | "doordashed", 16 | ]; 17 | 18 | Deno.bench({ 19 | name: "Async Exceptions", 20 | group: "Async::Composition", 21 | fn: async () => { 22 | for (const i of INPUTS) { 23 | try { 24 | await AsyncExceptions.processString(i); 25 | } catch (e) { 26 | } 27 | } 28 | }, 29 | }); 30 | 31 | Deno.bench({ 32 | name: "Task Operator Composition", 33 | group: "Async::Composition", 34 | fn: async () => { 35 | for (const i of INPUTS) { 36 | await TaskFlows.operatorComposition(i); 37 | } 38 | }, 39 | }); 40 | 41 | Deno.bench({ 42 | name: "Task Early Return", 43 | group: "Async::Composition", 44 | fn: async () => { 45 | for (const i of INPUTS) { 46 | await TaskFlows.earlyReturn(i); 47 | } 48 | }, 49 | }); 50 | 51 | Deno.bench({ 52 | name: "Task Instance Composition", 53 | group: "Async::Composition", 54 | baseline: true, 55 | fn: async () => { 56 | for (const i of INPUTS) { 57 | await TaskFlows.instanceComposition(i); 58 | } 59 | }, 60 | }); 61 | 62 | function sleep(ms: number): Promise { 63 | return new Promise((resolve) => setTimeout(resolve, ms)); 64 | } 65 | 66 | namespace AsyncExceptions { 67 | async function toUpperCase(input: string | undefined): Promise { 68 | if (typeof input === "undefined") { 69 | throw new TypeError("Input is undefined"); 70 | } 71 | await sleep(1); 72 | return input.toUpperCase(); 73 | } 74 | 75 | async function stringToLength(input: string): Promise { 76 | if (input.length === 0) { 77 | throw new TypeError("Input string is empty"); 78 | } 79 | await sleep(1); 80 | return input.length; 81 | } 82 | 83 | async function powerOfSelf(input: number): Promise { 84 | if ([Infinity, -Infinity, NaN].includes(input)) { 85 | throw new TypeError("Input is not a valid number"); 86 | } 87 | 88 | await sleep(1); 89 | const res = Math.pow(input, input); 90 | 91 | if ([Infinity, -Infinity, NaN].includes(res)) { 92 | throw new TypeError("Cannot calculate result"); 93 | } 94 | 95 | return res; 96 | } 97 | 98 | export async function processString( 99 | input: string | undefined, 100 | ): Promise { 101 | try { 102 | const upperCased = await toUpperCase(input); 103 | const length = await stringToLength(upperCased); 104 | return await powerOfSelf(length); 105 | } catch (error) { 106 | throw error; 107 | } 108 | } 109 | } 110 | 111 | namespace TaskFlows { 112 | function toUpperCase(input: string | undefined): Task { 113 | return Option(input) 114 | .okOrElse(() => TypeError("Input is undefined")) 115 | .into(Task.of) 116 | .map(async (str) => { 117 | await sleep(1); 118 | return str.toUpperCase(); 119 | }); 120 | } 121 | 122 | function stringToLength(input: string): Task { 123 | return Option.fromCoercible(input) 124 | .okOrElse(() => TypeError("Input string is empty")) 125 | .into(Task.of) 126 | .map(async (str) => { 127 | await sleep(1); 128 | return str.length; 129 | }); 130 | } 131 | 132 | function powerOfSelf(input: number): Task { 133 | return Option.fromCoercible(input) 134 | .okOrElse(() => TypeError("Input is not a valid number")) 135 | .into(Task.of) 136 | .andThen(async (n) => { 137 | await sleep(1); 138 | return Option.fromCoercible(Math.pow(n, n)) 139 | .okOrElse(() => TypeError("Cannot calculate result")); 140 | }); 141 | } 142 | 143 | export function instanceComposition( 144 | input: string | undefined, 145 | ): Task { 146 | return toUpperCase(input) 147 | .andThen(stringToLength) 148 | .andThen(powerOfSelf); 149 | } 150 | 151 | export async function operatorComposition( 152 | input: string | undefined, 153 | ): Promise> { 154 | return toUpperCase(input) 155 | .then(TaskOps.andThen(stringToLength)) 156 | .then(TaskOps.andThen(powerOfSelf)); 157 | } 158 | 159 | export async function earlyReturn( 160 | input: string | undefined, 161 | ): Promise> { 162 | const resStr = await toUpperCase(input); 163 | if (resStr.isErr()) return resStr; 164 | const resNum = await stringToLength(resStr.unwrap()); 165 | if (resNum.isErr()) return resNum; 166 | 167 | return powerOfSelf(resNum.unwrap()); 168 | } 169 | } 170 | -------------------------------------------------------------------------------- /lib/async/tasks.ts: -------------------------------------------------------------------------------- 1 | import { Result, Results } from "../core/result.ts"; 2 | import { Task } from "./task.ts"; 3 | 4 | /** 5 | * @module 6 | * 7 | * Utilities to work with collections of Task 8 | * 9 | * @category Task::Intermediate 10 | */ 11 | 12 | /** 13 | * Use this to collect all `Ok` values from an `Array>` or 14 | * `Iterable>` into an `Task`. 15 | * Upon encountring the first `Err` value, this value is returned. 16 | * 17 | * This function also works on variadic tuples and preserves the individual 18 | * types of the tuple members. 19 | * 20 | * @category Task::Intermediate 21 | * 22 | * @example 23 | * ```typescript 24 | * import { Task, Tasks } from "./mod.ts"; 25 | * import { Result } from "../core/result.ts"; 26 | * 27 | * const str = "thing" as string | TypeError; 28 | * const num = 5 as number | RangeError; 29 | * const bool = true as boolean | ReferenceError; 30 | * 31 | * const tuple = [ 32 | * Task.of(Result(str)), 33 | * Task.of(Result(num)), 34 | * Task.of(Result(bool)), 35 | * ] as const; 36 | * 37 | * const res: Result< 38 | * readonly [string, number, boolean], 39 | * TypeError | RangeError | ReferenceError 40 | * > = await Tasks.all(tuple); 41 | * ``` 42 | */ 43 | export function all< 44 | P extends Readonly>>>, 45 | >( 46 | tasks: P, 47 | ): Task, InferredFailureUnion

>; 48 | export function all( 49 | tasks: Readonly>>>, 50 | ): Task; 51 | //deno-lint-ignore no-explicit-any 52 | export function all(tasks: any): any { 53 | return Task.of(Promise.all(tasks).then((res) => Results.all(res))); 54 | } 55 | 56 | /** 57 | * Use this to obtain the first found `Ok` from an `Array>` or 58 | * `Iterable>`. 59 | * If no `Ok` value is found, the `Err` values are collected into an 60 | * array and returned. 61 | * 62 | * This function also works on variadic tuples and preserves the individual 63 | * types of the tuple members. 64 | * 65 | * @category Result::Intermediate 66 | * 67 | * @example 68 | * ```typescript 69 | * import { Task, Tasks } from "./mod.ts"; 70 | * import { Result } from "../core/result.ts"; 71 | * 72 | * const str = "thing" as string | TypeError; 73 | * const num = 5 as number | RangeError; 74 | * const bool = true as boolean | ReferenceError; 75 | * 76 | * const tuple = [ 77 | * Task.of(Result(str)), 78 | * Task.of(Result(num)), 79 | * Task.of(Result(bool)), 80 | * ] as const; 81 | * 82 | * const res: Result< 83 | * string | number | boolean, 84 | * readonly [TypeError, RangeError, ReferenceError] 85 | * > = await Tasks.any(tuple); 86 | * ``` 87 | */ 88 | export function any< 89 | P extends Readonly>>>, 90 | >( 91 | tasks: P, 92 | ): Task, InferredFailureTuple

>; 93 | export function any( 94 | tasks: Readonly>>>, 95 | ): Task; 96 | //deno-lint-ignore no-explicit-any 97 | export function any(tasks: any): any { 98 | return Task.of(Promise.all(tasks).then((res) => Results.any(res))); 99 | } 100 | 101 | /** 102 | * Use this to infer the encapsulated `` type from a `Task` 103 | * 104 | * @category Task::Basic 105 | */ 106 | export type InferredSuccessType

= P extends 107 | PromiseLike> ? T 108 | : never; 109 | 110 | /** 111 | * Use this to infer the encapsulated `` type from a `Task` 112 | * 113 | * @category Task::Basic 114 | */ 115 | export type InferredFailureType

= P extends 116 | PromiseLike> ? E 117 | : never; 118 | 119 | /** 120 | * Use this to infer the encapsulated `` types from a tuple of `Task` 121 | * 122 | * @category Task::Intermediate 123 | */ 124 | export type InferredSuccessTuple< 125 | P extends Readonly>>>, 126 | > = { 127 | [i in keyof P]: P[i] extends PromiseLike> ? T 128 | : never; 129 | }; 130 | 131 | /** 132 | * Use this to infer the encapsulated `` types from a tuple of `Task` 133 | * 134 | * @category Task::Intermediate 135 | */ 136 | export type InferredFailureTuple< 137 | P extends Readonly>>>, 138 | > = { 139 | [i in keyof P]: P[i] extends PromiseLike> ? E 140 | : never; 141 | }; 142 | 143 | /** 144 | * Use this to infer a union of all encapsulated `` types from a tuple of `Task` 145 | * 146 | * @category Task::Intermediate 147 | */ 148 | export type InferredSuccessUnion< 149 | P extends Readonly>>>, 150 | > = InferredSuccessTuple

[number]; 151 | 152 | /** 153 | * Use this to infer a union of all encapsulated `` types from a tuple of `Task` 154 | * 155 | * @category Task::Intermediate 156 | */ 157 | export type InferredFailureUnion< 158 | P extends Readonly>>>, 159 | > = InferredFailureTuple

[number]; 160 | -------------------------------------------------------------------------------- /lib/async/_internal.ts: -------------------------------------------------------------------------------- 1 | import { Err, Ok } from "../core/mod.ts"; 2 | import type { Result } from "../core/mod.ts"; 3 | 4 | /** 5 | * These are only used internally to save some chars 6 | * @interal 7 | */ 8 | 9 | type Future = PromiseLike>; 10 | type Either = Result | Future; 11 | 12 | export type ExecutorFn = ConstructorParameters< 13 | typeof Promise> 14 | >[0]; 15 | 16 | /** 17 | * @internal 18 | */ 19 | export async function idTask(task: Either): Promise> { 20 | return await task; 21 | } 22 | 23 | /** 24 | * @internal 25 | */ 26 | export async function cloneTask( 27 | task: Either, 28 | ): Promise> { 29 | return (await task).clone(); 30 | } 31 | 32 | /** 33 | * @internal 34 | */ 35 | export async function mapTaskSuccess( 36 | task: Either, 37 | mapFn: (v: T) => T2 | PromiseLike, 38 | ): Promise> { 39 | const res = await task; 40 | if (res.isErr()) return res; 41 | 42 | const mapped = await mapFn(res.unwrap()); 43 | 44 | return Ok(mapped); 45 | } 46 | 47 | /** 48 | * @internal 49 | */ 50 | export async function mapTaskSuccessOr( 51 | task: Either, 52 | mapFn: (v: T) => T2 | PromiseLike, 53 | orValue: T2 | PromiseLike, 54 | ): Promise> { 55 | const res = await task; 56 | 57 | const mapped = res.isErr() ? await orValue : await mapFn(res.unwrap()); 58 | 59 | return Ok(mapped); 60 | } 61 | 62 | /** 63 | * @internal 64 | */ 65 | export async function mapTaskSuccessOrElse( 66 | task: Either, 67 | mapFn: (v: T) => T2 | PromiseLike, 68 | orFn: (e: E) => T2 | PromiseLike, 69 | ): Promise> { 70 | const res = await task; 71 | 72 | const mapped = res.isErr() 73 | ? await orFn(res.unwrap()) 74 | : await mapFn(res.unwrap()); 75 | 76 | return Ok(mapped); 77 | } 78 | 79 | /** 80 | * @internal 81 | */ 82 | export async function mapTaskFailure( 83 | task: Either, 84 | mapFn: (v: E) => E2 | PromiseLike, 85 | ): Promise> { 86 | const res = await task; 87 | if (res.isOk()) return res; 88 | 89 | const mappedErr = await mapFn(res.unwrap()); 90 | 91 | return Err(mappedErr); 92 | } 93 | 94 | /** 95 | * @internal 96 | */ 97 | export async function chainTaskSuccess( 98 | task: Either, 99 | thenFn: (v: T) => Either, 100 | ): Promise> { 101 | const res = await task; 102 | if (res.isErr()) return res; 103 | 104 | return thenFn(res.unwrap()); 105 | } 106 | 107 | /** 108 | * @internal 109 | */ 110 | export async function chainTaskFailure( 111 | task: Either, 112 | elseFn: (v: E) => Either, 113 | ): Promise> { 114 | const res = await task; 115 | if (res.isOk()) return res; 116 | 117 | return elseFn(res.unwrap()); 118 | } 119 | 120 | /** 121 | * @internal 122 | */ 123 | export async function tapTask( 124 | task: Either, 125 | tapFn: (v: Result) => void | PromiseLike, 126 | ): Promise> { 127 | const res = (await task).clone(); 128 | 129 | await tapFn(res); 130 | 131 | return task; 132 | } 133 | 134 | /** 135 | * @internal 136 | */ 137 | export async function inspectTaskSuccess( 138 | task: Either, 139 | inspectFn: (v: T) => void | PromiseLike, 140 | ): Promise> { 141 | const res = await task; 142 | 143 | if (res.isOk()) { 144 | await inspectFn(res.unwrap()); 145 | } 146 | 147 | return task; 148 | } 149 | 150 | /** 151 | * @internal 152 | */ 153 | export async function inspectTaskFailure( 154 | task: Either, 155 | inspectFn: (v: E) => void | PromiseLike, 156 | ): Promise> { 157 | const res = await task; 158 | 159 | if (res.isErr()) { 160 | await inspectFn(res.unwrap()); 161 | } 162 | 163 | return task; 164 | } 165 | 166 | /** 167 | * @internal 168 | */ 169 | export async function zipTask( 170 | task1: Either, 171 | task2: Either, 172 | ): Promise> { 173 | return (await task1).zip(await task2); 174 | } 175 | 176 | /** 177 | * @internal 178 | */ 179 | export async function andEnsureTask( 180 | task: Either, 181 | ensureFn: (value: T) => Either, 182 | ): Promise> { 183 | const original = await task; 184 | 185 | if (original.isErr()) return original; 186 | 187 | const res = await ensureFn(original.unwrap()); 188 | 189 | return res.and(original); 190 | } 191 | 192 | /** 193 | * @internal 194 | */ 195 | export async function orEnsureTask( 196 | task: Either, 197 | ensureFn: (value: E) => Either, 198 | ): Promise> { 199 | const original = await task; 200 | 201 | if (original.isOk()) return original; 202 | 203 | const res = await ensureFn(original.unwrap()); 204 | 205 | return res.or(original); 206 | } 207 | 208 | /** 209 | * @internal 210 | */ 211 | export async function unwrapTask(task: Either): Promise { 212 | return (await task).unwrap(); 213 | } 214 | 215 | /** 216 | * @internal 217 | */ 218 | export async function unwrapTaskOr( 219 | task: Either, 220 | orValue: T2 | PromiseLike, 221 | ): Promise { 222 | const res = await task; 223 | 224 | if (res.isOk()) return res.unwrap(); 225 | return orValue; 226 | } 227 | 228 | /** 229 | * @internal 230 | */ 231 | export async function unwrapTaskOrElse( 232 | task: Either, 233 | orFn: (e: E) => T2 | PromiseLike, 234 | ): Promise { 235 | const res = await task; 236 | 237 | if (res.isOk()) return res.unwrap(); 238 | return orFn(res.unwrap()); 239 | } 240 | 241 | /** 242 | * @internal 243 | */ 244 | export async function* iterTask( 245 | task: Either, 246 | ): AsyncIterableIterator { 247 | const res = await task; 248 | 249 | if (res.isErr()) return; 250 | 251 | yield res.unwrap(); 252 | } 253 | -------------------------------------------------------------------------------- /lib/async/operators.ts: -------------------------------------------------------------------------------- 1 | import type { Result } from "../core/mod.ts"; 2 | import { 3 | andEnsureTask, 4 | chainTaskFailure, 5 | chainTaskSuccess, 6 | cloneTask, 7 | idTask, 8 | inspectTaskFailure, 9 | inspectTaskSuccess, 10 | iterTask, 11 | mapTaskFailure, 12 | mapTaskSuccess, 13 | mapTaskSuccessOr, 14 | mapTaskSuccessOrElse, 15 | orEnsureTask, 16 | tapTask, 17 | unwrapTask, 18 | unwrapTaskOr, 19 | unwrapTaskOrElse, 20 | zipTask, 21 | } from "./_internal.ts"; 22 | 23 | /** 24 | * ====================== 25 | * TASK ASYNC OPERATORS 26 | * ====================== 27 | */ 28 | 29 | /** 30 | * Use this to obtain a function, with returns the provided `Task` itself. 31 | * Canonical identity function. 32 | * 33 | * Mostly useful for flattening or en lieu of a noop. 34 | * 35 | * This is mostly provided for compatibility with with `Result`. 36 | * 37 | * @category Task::Basic 38 | * 39 | * @deprecated (will be removed in 1.0.0) use {@linkcode Task} instead 40 | */ 41 | export function id() { 42 | return function (res: Result | PromiseLike>) { 43 | return idTask(res); 44 | }; 45 | } 46 | 47 | /** 48 | * @deprecated (will be removed in 1.0.0) use {@linkcode Task} instead 49 | */ 50 | export function clone() { 51 | return function (res: Result | PromiseLike>) { 52 | return cloneTask(res); 53 | }; 54 | } 55 | 56 | /** 57 | * @deprecated (will be removed in 1.0.0) use {@linkcode Task} instead 58 | */ 59 | export function map(mapFn: (v: T) => T2 | PromiseLike) { 60 | return function (res: Result | PromiseLike>) { 61 | return mapTaskSuccess(res, mapFn); 62 | }; 63 | } 64 | 65 | /** 66 | * @deprecated (will be removed in 1.0.0) use {@linkcode Task} instead 67 | */ 68 | export function mapOr( 69 | mapFn: (v: T) => T2 | PromiseLike, 70 | orValue: T2 | PromiseLike, 71 | ) { 72 | return function (res: Result | PromiseLike>) { 73 | return mapTaskSuccessOr(res, mapFn, orValue); 74 | }; 75 | } 76 | 77 | /** 78 | * @deprecated (will be removed in 1.0.0) use {@linkcode Task} instead 79 | */ 80 | export function mapOrElse( 81 | mapFn: (v: T) => T2 | PromiseLike, 82 | elseFn: (e: E) => T2 | PromiseLike, 83 | ) { 84 | return function (res: Result | PromiseLike>) { 85 | return mapTaskSuccessOrElse(res, mapFn, elseFn); 86 | }; 87 | } 88 | 89 | /** 90 | * @deprecated (will be removed in 1.0.0) use {@linkcode Task} instead 91 | */ 92 | export function mapErr(mapFn: (v: E) => E2 | PromiseLike) { 93 | return function (res: Result | PromiseLike>) { 94 | return mapTaskFailure(res, mapFn); 95 | }; 96 | } 97 | 98 | /** 99 | * @deprecated (will be removed in 1.0.0) use {@linkcode Task} instead 100 | */ 101 | export function andThen( 102 | thenFn: (v: T) => Result | PromiseLike>, 103 | ) { 104 | return function (res: Result | PromiseLike>) { 105 | return chainTaskSuccess(res, thenFn); 106 | }; 107 | } 108 | 109 | /** 110 | * @deprecated (will be removed in 1.0.0) use {@linkcode Task} instead 111 | */ 112 | export function orElse( 113 | elseFn: (v: E) => Result | PromiseLike>, 114 | ) { 115 | return function (res: Result | PromiseLike>) { 116 | return chainTaskFailure(res, elseFn); 117 | }; 118 | } 119 | 120 | /** 121 | * @deprecated (will be removed in 1.0.0) use {@linkcode Task} instead 122 | */ 123 | export function zip( 124 | rhs: Result | PromiseLike>, 125 | ) { 126 | return function (res: Result | PromiseLike>) { 127 | return zipTask(res, rhs); 128 | }; 129 | } 130 | 131 | /** 132 | * @deprecated (will be removed in 1.0.0) use {@linkcode Task} instead 133 | */ 134 | export function tap( 135 | tapFn: (v: Result) => void | PromiseLike, 136 | ) { 137 | return function (res: Result | PromiseLike>) { 138 | return tapTask(res, tapFn); 139 | }; 140 | } 141 | 142 | /** 143 | * @deprecated (will be removed in 1.0.0) use {@linkcode Task} instead 144 | */ 145 | export function inspect(inspectFn: (v: T) => void | PromiseLike) { 146 | return function (res: Result | PromiseLike>) { 147 | return inspectTaskSuccess(res, inspectFn); 148 | }; 149 | } 150 | 151 | /** 152 | * @deprecated (will be removed in 1.0.0) use {@linkcode Task} instead 153 | */ 154 | export function inspectErr( 155 | inspectFn: (v: E) => void | PromiseLike, 156 | ) { 157 | return function (res: Result | PromiseLike>) { 158 | return inspectTaskFailure(res, inspectFn); 159 | }; 160 | } 161 | 162 | /** 163 | * @deprecated (will be removed in 1.0.0) use {@linkcode Task} instead 164 | */ 165 | export function trip( 166 | tripFn: (e: T) => Result | PromiseLike>, 167 | ) { 168 | return function (res: Result | PromiseLike>) { 169 | return andEnsureTask(res, tripFn); 170 | }; 171 | } 172 | 173 | /** 174 | * @deprecated (will be removed in 1.0.0) use {@linkcode Task} instead 175 | */ 176 | export function rise( 177 | riseFn: (e: E) => Result | PromiseLike>, 178 | ) { 179 | return function (res: Result | PromiseLike>) { 180 | return orEnsureTask(res, riseFn); 181 | }; 182 | } 183 | 184 | /** 185 | * @deprecated (will be removed in 1.0.0) use {@linkcode Task} instead 186 | */ 187 | export function unwrap() { 188 | return function (res: Result | PromiseLike>) { 189 | return unwrapTask(res); 190 | }; 191 | } 192 | 193 | /** 194 | * @deprecated (will be removed in 1.0.0) use {@linkcode Task} instead 195 | */ 196 | export function unwrapOr(orValue: T2 | PromiseLike) { 197 | return function (res: Result | PromiseLike>) { 198 | return unwrapTaskOr(res, orValue); 199 | }; 200 | } 201 | 202 | /** 203 | * @deprecated (will be removed in 1.0.0) use {@linkcode Task} instead 204 | */ 205 | export function unwrapOrElse(elseFn: (e: E) => T2 | PromiseLike) { 206 | return function (res: Result | PromiseLike>) { 207 | return unwrapTaskOrElse(res, elseFn); 208 | }; 209 | } 210 | 211 | /** 212 | * @deprecated (will be removed in 1.0.0) use {@linkcode Task} instead 213 | */ 214 | export function iter() { 215 | return function (res: Result | PromiseLike>) { 216 | return iterTask(res); 217 | }; 218 | } 219 | -------------------------------------------------------------------------------- /lib/async/tasks_test.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | InferredFailureTuple, 3 | InferredFailureType, 4 | InferredFailureUnion, 5 | InferredSuccessTuple, 6 | InferredSuccessType, 7 | InferredSuccessUnion, 8 | } from "./tasks.ts"; 9 | import { Task, Tasks } from "./mod.ts"; 10 | import { Err, Ok } from "../core/mod.ts"; 11 | import { 12 | assertEquals, 13 | assertStrictEquals, 14 | assertType, 15 | } from "../../dev_deps.ts"; 16 | import type { IsExact } from "../../dev_deps.ts"; 17 | 18 | Deno.test("eitherway::Task::InferredTypes", async (t) => { 19 | await t.step( 20 | "InferredSuccessType

-> inferres encapsulated type correctly", 21 | () => { 22 | const task = Task.succeed("str"); 23 | 24 | assertType, string>>(true); 25 | }, 26 | ); 27 | 28 | await t.step( 29 | "InferredFailureType

-> inferres encapsulated type correctly", 30 | () => { 31 | const task = Task.fail(TypeError("This is the one")); 32 | 33 | assertType, TypeError>>(true); 34 | }, 35 | ); 36 | 37 | await t.step( 38 | "InferredSuccessTuple

-> maps inferred tuples correctly", 39 | () => { 40 | const one = Task.succeed(1); 41 | const two = Task.succeed(true); 42 | const three = Task.fail("str"); 43 | const tuple = [one, two, three] as const; 44 | 45 | assertType< 46 | IsExact< 47 | InferredSuccessTuple, 48 | readonly [number, boolean, never] 49 | > 50 | >(true); 51 | }, 52 | ); 53 | 54 | await t.step( 55 | "InferredFailureTuple

-> maps inferred tuples correctly", 56 | () => { 57 | const one = Task.fail("1"); 58 | const two = Task.fail(Error()); 59 | const three = Task.succeed(TypeError()); 60 | const tuple = [one, two, three] as const; 61 | 62 | assertType< 63 | IsExact< 64 | InferredFailureTuple, 65 | readonly [string, Error, never] 66 | > 67 | >(true); 68 | }, 69 | ); 70 | 71 | await t.step( 72 | "InferredSuccessUnion

-> maps inferred tuples correctly to union", 73 | () => { 74 | const one = Task.succeed(1); 75 | const two = Task.succeed(true); 76 | const three = Task.succeed("str"); 77 | const tuple = [one, two, three] as const; 78 | 79 | assertType< 80 | IsExact, number | boolean | string> 81 | >(true); 82 | }, 83 | ); 84 | 85 | await t.step( 86 | "InferredFailureUnion

-> maps inferred tuples correctly to union", 87 | () => { 88 | const one = Task.fail("1"); 89 | const two = Task.fail(Error()); 90 | const three = Task.fail(TypeError()); 91 | const tuple = [one, two, three] as const; 92 | 93 | assertType< 94 | IsExact, string | Error | TypeError> 95 | >(true); 96 | }, 97 | ); 98 | }); 99 | 100 | Deno.test("eitherway::Tasks", async (t) => { 101 | await t.step(".all() -> collects Ok values into an array", async () => { 102 | const collection: Task[] = Array(5).fill(0).map((v, i) => 103 | Task.succeed(v + i) 104 | ); 105 | 106 | const success = Tasks.all(collection); 107 | const ok = await success; 108 | 109 | assertType>>(true); 110 | assertEquals(ok.unwrap(), [0, 1, 2, 3, 4]); 111 | }); 112 | 113 | await t.step( 114 | ".all() -> returns the first instance of Err if present in iterable", 115 | async () => { 116 | const error = Err(TypeError("This is the one")); 117 | const collection: Task[] = Array(5).fill(0).map( 118 | (v, i) => { 119 | if (i === 3) return Task.of(error); 120 | return Task.succeed(v + i); 121 | }, 122 | ); 123 | 124 | const task = Tasks.all(collection); 125 | const err = await task; 126 | 127 | assertType>>(true); 128 | assertStrictEquals(err.isErr(), true); 129 | assertStrictEquals(error, err); 130 | }, 131 | ); 132 | 133 | await t.step( 134 | ".all() -> returns the correct type when used on tuples", 135 | async () => { 136 | const str = Task.succeed("str") as Task; 137 | const num = Task.succeed(123) as Task; 138 | const bool = Task.succeed(true) as Task; 139 | const tuple = [str, num, bool] as const; 140 | 141 | const task = Tasks.all(tuple); 142 | const ok = await task; 143 | 144 | assertType< 145 | IsExact< 146 | typeof task, 147 | Task< 148 | readonly [string, number, boolean], 149 | TypeError | RangeError | Error 150 | > 151 | > 152 | >(true); 153 | assertStrictEquals(ok.isOk(), true); 154 | assertEquals(ok.unwrap(), ["str", 123, true]); 155 | }, 156 | ); 157 | 158 | await t.step(".any() -> collects all Err values into an array", async () => { 159 | const collection: Task[] = Array(5).fill(0).map((v, i) => 160 | Task.fail(v + i) 161 | ); 162 | 163 | const failure = Tasks.any(collection); 164 | const err = await failure; 165 | 166 | assertType>>(true); 167 | assertStrictEquals(err.isErr(), true); 168 | assertEquals(err.unwrap(), [0, 1, 2, 3, 4]); 169 | }); 170 | 171 | await t.step( 172 | ".any() -> returns the first instance of Ok if present in iterable", 173 | async () => { 174 | const success = Ok(42); 175 | const collection: Task[] = Array(5).fill(0).map( 176 | (_, i) => { 177 | if (i === 3) return Task.of(success); 178 | return Task.fail(TypeError()); 179 | }, 180 | ); 181 | 182 | const task = Tasks.any(collection); 183 | const ok = await task; 184 | 185 | assertType>>(true); 186 | assertStrictEquals(ok.isOk(), true); 187 | assertStrictEquals(ok, success); 188 | }, 189 | ); 190 | 191 | await t.step( 192 | ".any() -> returns the correct type when used on tuples", 193 | async () => { 194 | const str = Task.succeed("str") as Task; 195 | const num = Task.succeed(123) as Task; 196 | const bool = Task.succeed(true) as Task; 197 | const tuple = [str, num, bool] as const; 198 | 199 | const task = Tasks.any(tuple); 200 | const ok = await task; 201 | 202 | assertType< 203 | IsExact< 204 | typeof task, 205 | Task< 206 | string | number | boolean, 207 | readonly [TypeError, RangeError, Error] 208 | > 209 | > 210 | >(true); 211 | assertStrictEquals(ok.isOk(), true); 212 | assertEquals(ok.unwrap(), "str"); 213 | }, 214 | ); 215 | }); 216 | -------------------------------------------------------------------------------- /lib/adapters/web/fetch/mod_test.ts: -------------------------------------------------------------------------------- 1 | import { Task } from "../../../async/task.ts"; 2 | import { Err, Ok } from "../../../core/result.ts"; 3 | 4 | import { 5 | assertEquals, 6 | assertExists, 7 | assertInstanceOf, 8 | assertObjectMatch, 9 | assertStrictEquals, 10 | assertType, 11 | IsExact, 12 | } from "../../../../dev_deps.ts"; 13 | import { 14 | FailedRequest, 15 | FetchException, 16 | isFetchException, 17 | liftFetch, 18 | toFetchResult, 19 | } from "./mod.ts"; 20 | 21 | Deno.test("eitherway::adapters::web::fetch", async (t) => { 22 | await t.step("FailedRequest", async (t) => { 23 | await t.step(".from() -> produces a new generic instance", () => { 24 | const responseLike = { 25 | ok: false, 26 | status: 0, 27 | statusText: "NetworkError", 28 | } as const; 29 | type RL = typeof responseLike; 30 | 31 | const error = FailedRequest.from(responseLike); 32 | type Inner = ReturnType; 33 | 34 | assertType>(true); 35 | assertStrictEquals(error.name, "FailedRequest"); 36 | assertStrictEquals(error.status, responseLike.status); 37 | assertStrictEquals(error.statusText, responseLike.statusText); 38 | assertStrictEquals(error.getResponse(), responseLike); 39 | }); 40 | 41 | await t.step( 42 | ".with() -> produces a new instance with status properties", 43 | () => { 44 | const status = 500; 45 | const statusText = "Internal Server Error"; 46 | 47 | const error = FailedRequest.with(status, statusText); 48 | const internalResponse = error.getResponse(); 49 | 50 | assertType>>(true); 51 | assertType>(true); 52 | assertStrictEquals(error.status, internalResponse.status); 53 | assertStrictEquals(error.statusText, internalResponse.statusText); 54 | }, 55 | ); 56 | 57 | await t.step(".toJSON() -> produces a serializalbe representation", () => { 58 | const status = 500; 59 | const statusText = "Internal Server Error"; 60 | 61 | const error = FailedRequest.with(status, statusText); 62 | 63 | assertStrictEquals( 64 | JSON.stringify(error), 65 | JSON.stringify({ status, statusText }), 66 | ); 67 | }); 68 | }); 69 | 70 | await t.step("FetchException", async (t) => { 71 | await t.step(".from() -> produces new instance", () => { 72 | const cause = "Something went wrong"; 73 | const expectedCause = Error("Unknown exception", { cause }); 74 | 75 | const exception = FetchException.from(cause); 76 | 77 | assertEquals(exception.cause, expectedCause); 78 | assertStrictEquals(exception.name, "FetchException"); 79 | }); 80 | 81 | await t.step("isFetchException() -> narrows to FetchException", () => { 82 | const exception = FetchException.from("Something went wrong"); 83 | const plainError = Error("Something went wrong"); 84 | const unkwn = "Unknown" as unknown; 85 | 86 | if (isFetchException(unkwn)) { 87 | assertType>(true); 88 | } 89 | 90 | assertStrictEquals(isFetchException(exception), true); 91 | assertStrictEquals(isFetchException(plainError), false); 92 | assertStrictEquals(isFetchException(unkwn), false); 93 | assertStrictEquals(isFetchException(undefined), false); 94 | }); 95 | }); 96 | 97 | await t.step("toFetchResult", async (t) => { 98 | await t.step( 99 | "() -> returns an instance of Err> for non-ok response like inputs", 100 | () => { 101 | const errorResponse = Response.error(); 102 | 103 | const err = toFetchResult(errorResponse); 104 | const unwrapped = err.unwrap(); 105 | 106 | assertType< 107 | IsExact> 108 | >(true); 109 | assertStrictEquals(err.isErr(), true); 110 | assertEquals(unwrapped, FailedRequest.from(errorResponse)); 111 | }, 112 | ); 113 | 114 | await t.step( 115 | "() -> returns an instance of Ok for ok response like inputs", 116 | () => { 117 | const response = Response.json({ id: 1 }); 118 | 119 | const ok = toFetchResult(response); 120 | const unwrapped = ok.unwrap(); 121 | 122 | assertType< 123 | IsExact> 124 | >(true); 125 | assertStrictEquals(ok.isErr(), false); 126 | assertStrictEquals(unwrapped, response); 127 | }, 128 | ); 129 | }); 130 | 131 | await t.step("liftFetch", async (t) => { 132 | /* 133 | * Shared interfaces 134 | */ 135 | interface User { 136 | id: number; 137 | } 138 | interface UserResponse extends Response { 139 | json(): Promise; 140 | } 141 | 142 | await t.step( 143 | "() -> lifts untyped fetch with default errMapFn and ctor", 144 | async () => { 145 | const testUrl = "https://jsonplaceholder.typicode.com/users/1"; 146 | 147 | const lifted = liftFetch(fetch); 148 | const result = await lifted(testUrl) 149 | .trip(() => Task.succeed("Just to test")) 150 | .inspectErr(console.error) 151 | .map((response) => response.json()); 152 | 153 | assertType< 154 | IsExact, Parameters> 155 | >(true); 156 | assertType< 157 | IsExact< 158 | ReturnType, 159 | Task | FetchException> 160 | > 161 | >(true); 162 | assertStrictEquals(result.isOk(), true); 163 | assertExists(result.unwrap()); 164 | }, 165 | ); 166 | 167 | await t.step( 168 | "() -> lifts typed fetch with defauft errMapFn and ctor", 169 | async () => { 170 | async function getUser(id: number): Promise { 171 | await new Promise((resolve) => setTimeout(resolve, 10)); 172 | return Response.json({ id }); 173 | } 174 | 175 | const lifted = liftFetch(getUser); 176 | 177 | const result = await lifted(1) 178 | .map((response) => response.json()) 179 | .inspect((user) => assertType>(true)); 180 | 181 | assertType< 182 | IsExact, Parameters> 183 | >(true); 184 | assertType< 185 | IsExact< 186 | ReturnType, 187 | Task< 188 | UserResponse, 189 | FailedRequest | FetchException 190 | > 191 | > 192 | >(true); 193 | assertStrictEquals(result.isOk(), true); 194 | assertEquals(result.unwrap(), { id: 1 }); 195 | }, 196 | ); 197 | 198 | await t.step( 199 | "() -> propagates errors correctly via the Err path", 200 | async () => { 201 | async function getUserNotFound(_id: number): Promise { 202 | await new Promise((resolve) => setTimeout(resolve, 10)); 203 | return new Response(null, { status: 404, statusText: "Not Found" }); 204 | } 205 | 206 | const lifted = liftFetch(getUserNotFound); 207 | 208 | const result = await lifted(11); 209 | 210 | assertType< 211 | IsExact, Parameters> 212 | >(true); 213 | assertType< 214 | IsExact< 215 | ReturnType, 216 | Task< 217 | UserResponse, 218 | FailedRequest | FetchException 219 | > 220 | > 221 | >(true); 222 | assertStrictEquals(result.isErr(), true); 223 | assertInstanceOf(result.unwrap(), FailedRequest); 224 | assertEquals(result.unwrap(), FailedRequest.with(404, "Not Found")); 225 | }, 226 | ); 227 | 228 | await t.step( 229 | "() -> propagates exceptions correctly via the Err path", 230 | async () => { 231 | async function failingFetch(_id: number): Promise { 232 | await new Promise((resolve) => setTimeout(resolve, 100)); 233 | throw new Error("Network error"); 234 | } 235 | 236 | const lifted = liftFetch(failingFetch); 237 | 238 | const result = await lifted(11); 239 | 240 | assertType< 241 | IsExact, Parameters> 242 | >(true); 243 | assertType< 244 | IsExact< 245 | ReturnType, 246 | Task< 247 | UserResponse, 248 | FailedRequest | FetchException 249 | > 250 | > 251 | >(true); 252 | assertStrictEquals(result.isErr(), true); 253 | assertInstanceOf(result.unwrap(), FetchException); 254 | assertEquals( 255 | result.unwrap(), 256 | FetchException.from(Error("Network error")), 257 | ); 258 | }, 259 | ); 260 | 261 | await t.step( 262 | "() -> lifts provided fetch like fn with custom errMapFn and ctor", 263 | async () => { 264 | const testUrl = "https://jsonplaceholder.typicode.com/users/1"; 265 | async function ctor(res: UserResponse) { 266 | if (res.ok) return Ok(await res.json()); 267 | return Err(TypeError("Oh no", { cause: res })); 268 | } 269 | const errMapFn = (cause: unknown) => ReferenceError("Dunno", { cause }); 270 | 271 | const lifted = liftFetch(fetch, ctor, errMapFn); 272 | const result = await lifted(testUrl); 273 | 274 | assertType< 275 | IsExact, Parameters> 276 | >(true); 277 | assertType< 278 | IsExact< 279 | ReturnType, 280 | Task< 281 | User, 282 | TypeError | ReferenceError 283 | > 284 | > 285 | >(true); 286 | assertStrictEquals(result.isOk(), true); 287 | assertObjectMatch(result.unwrap(), { id: 1 }); 288 | }, 289 | ); 290 | }); 291 | }); 292 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # Contributing to eitherway 4 | 5 | > Generated with [contributing.md](https://contributing.md/) 6 | 7 | First off, thanks for taking the time to contribute! ❤️ 8 | 9 | All types of contributions are encouraged and valued. See the 10 | [Table of Contents](#table-of-contents) for different ways to help and details 11 | about how this project handles them. Please make sure to read the relevant 12 | section before making your contribution. It will make it a lot easier for us 13 | maintainers and smooth out the experience for all involved. The community looks 14 | forward to your contributions. 🎉 15 | 16 | > And if you like the project, but just don't have time to contribute, that's 17 | > fine. There are other easy ways to support the project and show your 18 | > appreciation, which we would also be very happy about: 19 | > 20 | > - Star the project 21 | > - Tweet about it 22 | > - Refer this project in your project's readme 23 | > - Mention the project at local meetups and tell your friends/colleagues 24 | 25 | 26 | 27 | ## Table of Contents 28 | 29 | - [I Have a Question](#i-have-a-question) 30 | - [I Want To Contribute](#i-want-to-contribute) 31 | - [Reporting Bugs](#reporting-bugs) 32 | - [Suggesting Enhancements](#suggesting-enhancements) 33 | - [Your First Code Contribution](#your-first-code-contribution) 34 | - [Improving The Documentation](#improving-the-documentation) 35 | - [Styleguides](#styleguides) 36 | - [Commit Messages](#commit-messages) 37 | - [Join The Project Team](#join-the-project-team) 38 | 39 | ## I Have a Question 40 | 41 | > If you want to ask a question, we assume that you have read the available 42 | > [Documentation](https://deno.land/x/eitherway). 43 | 44 | Before you ask a question, it is best to search for existing 45 | [Issues](https://github.com/realpha/eitherway/issues) that might help you. In 46 | case you have found a suitable issue and still need clarification, you can write 47 | your question in this issue. It is also advisable to search the internet for 48 | answers first. 49 | 50 | If you then still feel the need to ask a question and need clarification, we 51 | recommend the following: 52 | 53 | - Open an [Issue](https://github.com/realpha/eitherway/issues/new). 54 | - Provide as much context as you can about what you're running into. 55 | - Provide project and platform versions (nodejs, npm, etc), depending on what 56 | seems relevant. 57 | 58 | We will then take care of the issue as soon as possible. 59 | 60 | 74 | 75 | ## I Want To Contribute 76 | 77 | > ### Legal Notice 78 | > 79 | > When contributing to this project, you must agree that you have authored 100% 80 | > of the content, that you have the necessary rights to the content and that the 81 | > content you contribute may be provided under the project license. 82 | 83 | ### Reporting Bugs 84 | 85 | 86 | 87 | #### Before Submitting a Bug Report 88 | 89 | A good bug report shouldn't leave others needing to chase you up for more 90 | information. Therefore, we ask you to investigate carefully, collect information 91 | and describe the issue in detail in your report. Please complete the following 92 | steps in advance to help us fix any potential bug as fast as possible. 93 | 94 | - Make sure that you are using the latest version. 95 | - Determine if your bug is really a bug and not an error on your side e.g. using 96 | incompatible environment components/versions (Make sure that you have read the 97 | [documentation](https://deno.land/x/eitherway). If you are looking for 98 | support, you might want to check [this section](#i-have-a-question)). 99 | - To see if other users have experienced (and potentially already solved) the 100 | same issue you are having, check if there is not already a bug report existing 101 | for your bug or error in the 102 | [bug tracker](https://github.com/realpha/eitherwayissues?q=label%3Abug). 103 | - Also make sure to search the internet (including Stack Overflow) to see if 104 | users outside of the GitHub community have discussed the issue. 105 | - Collect information about the bug: 106 | - Stack trace (Traceback) 107 | - OS, Platform and Version (Windows, Linux, macOS, x86, ARM) 108 | - Version of the interpreter, compiler, SDK, runtime environment, package 109 | manager, depending on what seems relevant. 110 | - Possibly your input and the output 111 | - Can you reliably reproduce the issue? And can you also reproduce it with 112 | older versions? 113 | 114 | 115 | 116 | #### How Do I Submit a Good Bug Report? 117 | 118 | > You must never report security related issues, vulnerabilities or bugs 119 | > including sensitive information to the issue tracker, or elsewhere in public. 120 | > Instead sensitive bugs must be sent by email to <>. 121 | 122 | 123 | 124 | We use GitHub issues to track bugs and errors. If you run into an issue with the 125 | project: 126 | 127 | - Open an [Issue](https://github.com/realpha/eitherway/issues/new). (Since we 128 | can't be sure at this point whether it is a bug or not, we ask you not to talk 129 | about a bug yet and not to label the issue.) 130 | - Explain the behavior you would expect and the actual behavior. 131 | - Please provide as much context as possible and describe the _reproduction 132 | steps_ that someone else can follow to recreate the issue on their own. This 133 | usually includes your code. For good bug reports you should isolate the 134 | problem and create a reduced test case. 135 | - Provide the information you collected in the previous section. 136 | 137 | Once it's filed: 138 | 139 | - The project team will label the issue accordingly. 140 | - A team member will try to reproduce the issue with your provided steps. If 141 | there are no reproduction steps or no obvious way to reproduce the issue, the 142 | team will ask you for those steps and mark the issue as `needs-repro`. Bugs 143 | with the `needs-repro` tag will not be addressed until they are reproduced. 144 | - If the team is able to reproduce the issue, it will be marked `needs-fix`, as 145 | well as possibly other tags (such as `critical`), and the issue will be left 146 | to be [implemented by someone](#your-first-code-contribution). 147 | 148 | 149 | 150 | ### Suggesting Enhancements 151 | 152 | This section guides you through submitting an enhancement suggestion for 153 | eitherway, **including completely new features and minor improvements to 154 | existing functionality**. Following these guidelines will help maintainers and 155 | the community to understand your suggestion and find related suggestions. 156 | 157 | 158 | 159 | #### Before Submitting an Enhancement 160 | 161 | - Make sure that you are using the latest version. 162 | - Read the [documentation](https://deno.land/x/eitherway) carefully and find out 163 | if the functionality is already covered, maybe by an individual configuration. 164 | - Perform a [search](https://github.com/realpha/eitherway/issues) to see if the 165 | enhancement has already been suggested. If it has, add a comment to the 166 | existing issue instead of opening a new one. 167 | - Find out whether your idea fits with the scope and aims of the project. It's 168 | up to you to make a strong case to convince the project's developers of the 169 | merits of this feature. Keep in mind that we want features that will be useful 170 | to the majority of our users and not just a small subset. If you're just 171 | targeting a minority of users, consider writing an add-on/plugin library. 172 | 173 | 174 | 175 | #### How Do I Submit a Good Enhancement Suggestion? 176 | 177 | Enhancement suggestions are tracked as 178 | [GitHub issues](https://github.com/realpha/eitherway/issues). 179 | 180 | - Use a **clear and descriptive title** for the issue to identify the 181 | suggestion. 182 | - Provide a **step-by-step description of the suggested enhancement** in as many 183 | details as possible. 184 | - **Describe the current behavior** and **explain which behavior you expected to 185 | see instead** and why. At this point you can also tell which alternatives do 186 | not work for you. 187 | - You may want to **include screenshots and animated GIFs** which help you 188 | demonstrate the steps or point out the part which the suggestion is related 189 | to. You can use [this tool](https://www.cockos.com/licecap/) to record GIFs on 190 | macOS and Windows, and [this tool](https://github.com/colinkeenan/silentcast) 191 | or [this tool](https://github.com/GNOME/byzanz) on Linux. 192 | 193 | - **Explain why this enhancement would be useful** to most eitherway users. You 194 | may also want to point out the other projects that solved it better and which 195 | could serve as inspiration. 196 | 197 | 198 | 199 | ### Your First Code Contribution 200 | 201 | 205 | 206 | ### Improving The Documentation 207 | 208 | 212 | 213 | ## Styleguides 214 | 215 | ### Commit Messages 216 | 217 | 220 | 221 | ## Join The Project Team 222 | 223 | 224 | 225 | 226 | 227 | ## Attribution 228 | 229 | This guide is based on the **contributing-gen**. 230 | [Make your own](https://github.com/bttger/contributing-gen)! 231 | -------------------------------------------------------------------------------- /lib/async/task.ts: -------------------------------------------------------------------------------- 1 | import { asInfallible, Err, Ok, Result } from "../core/result.ts"; 2 | import type { ExecutorFn } from "./_internal.ts"; 3 | import { 4 | andEnsureTask, 5 | chainTaskFailure, 6 | chainTaskSuccess, 7 | cloneTask, 8 | inspectTaskFailure, 9 | inspectTaskSuccess, 10 | iterTask, 11 | mapTaskFailure, 12 | mapTaskSuccess, 13 | mapTaskSuccessOr, 14 | mapTaskSuccessOrElse, 15 | orEnsureTask, 16 | tapTask, 17 | unwrapTask, 18 | unwrapTaskOr, 19 | unwrapTaskOrElse, 20 | zipTask, 21 | } from "./_internal.ts"; 22 | 23 | /** 24 | * This is the interface of the return value of {@linkcode Task.deferred} 25 | */ 26 | export interface DeferredTask { 27 | task: Task; 28 | succeed: (value: T) => void; 29 | fail: (error: E) => void; 30 | } 31 | 32 | /** 33 | * # Task 34 | * 35 | * `Task` is a composeable extension of `Promise>` 36 | * 37 | * It is a `Promise` sub-class, which never rejects, but always resolves. 38 | * Either with an `Ok` or an `Err` 39 | * 40 | * It supports almost the same API as {@linkcode Result} and allows for 41 | * the same composition patterns as {@linkcode Result} 42 | * 43 | * Furthermore, {@linkcode Tasks} exposes a few functions to ease working 44 | * with collections (indexed and plain `Iterable`s) 45 | * 46 | * @category Task::Basic 47 | */ 48 | export class Task extends Promise> { 49 | private constructor(executor: ExecutorFn) { 50 | super(executor); 51 | } 52 | 53 | /** 54 | * ======================= 55 | * TASK CONSTRUCTORS 56 | * ======================= 57 | */ 58 | 59 | /** 60 | * Use this to create a task from a `Result` value 61 | * 62 | * @category Task::Basic 63 | * 64 | * @example 65 | * ```typescript 66 | * import { Ok, Result, Task } from "https://deno.land/x/eitherway/mod.ts"; 67 | * 68 | * async function produceRes(): Promise> { 69 | * return Ok(42); 70 | * } 71 | * 72 | * const task = Task.of(produceRes()); 73 | * ``` 74 | */ 75 | static of( 76 | value: Result | PromiseLike>, 77 | ): Task { 78 | return new Task((resolve) => resolve(value)); 79 | } 80 | 81 | /** 82 | * Use this to create a `Task` which always succeeds with a value `` 83 | * 84 | * @category Task::Basic 85 | * 86 | * @example 87 | * ```typescript 88 | * import { Task } from "https://deno.land/x/eitherway/mod.ts"; 89 | * 90 | * const task: Task = Task.succeed(42); 91 | * ``` 92 | */ 93 | static succeed(value: T): Task { 94 | return new Task((resolve) => resolve(Ok(value))); 95 | } 96 | 97 | /** 98 | * Use this to create a `Task` which always fails with a value `` 99 | * 100 | * @category Task::Basic 101 | * 102 | * @example 103 | * ```typescript 104 | * import { Task } from "https://deno.land/x/eitherway/mod.ts"; 105 | * 106 | * const task: Task = Task.fail(1); 107 | * ``` 108 | */ 109 | static fail(error: E): Task { 110 | return new Task((resolve) => resolve(Err(error))); 111 | } 112 | 113 | /** 114 | * Use this to create a deferred `Task` which will either succeed with 115 | * a value of type `` or fail with a value of type `` 116 | * 117 | * You have to provide the generic types explicitly, otherwise `` will 118 | * be inferred as `` 119 | * 120 | * This is mostly useful when working with push-based APIs 121 | * 122 | * @category Task::Advanced 123 | * 124 | * @example 125 | * ```typescript 126 | * import { Task } from "./task.ts"; 127 | * 128 | * class TimeoutError extends Error {} 129 | * 130 | * const { task, succeed, fail } = Task.deferred(); 131 | * 132 | * setTimeout(() => succeed(42), Math.random() * 1000); 133 | * setTimeout(() => fail(new TimeoutError()), 500); 134 | * 135 | * await task 136 | * .inspect(console.log) 137 | * .inspectErr(console.error); 138 | * ``` 139 | */ 140 | static deferred(): DeferredTask { 141 | let resolveBinding: (res: Result) => void; 142 | const task = new Task((resolve) => { 143 | resolveBinding = resolve; 144 | }); 145 | const succeed = (value: T) => resolveBinding(Ok(value)); 146 | const fail = (error: E) => resolveBinding(Err(error)); 147 | 148 | return { task, succeed, fail }; 149 | } 150 | 151 | /** 152 | * Use this to create a task from a function which returns a `Result` 153 | * or `PromiseLike` value. 154 | * 155 | * This function should be infallible by contract. 156 | * 157 | * Use {@linkcode Task.fromFallible} if this is not the case. 158 | * 159 | * @category Task::Basic 160 | * 161 | * @example 162 | * ```typescript 163 | * import { Ok, Result, Task } from "https://deno.land/x/eitherway/mod.ts"; 164 | * 165 | * async function produceRes(): Promise> { 166 | * return Ok(42); 167 | * } 168 | * 169 | * const task = Task.from(produceRes); 170 | * ``` 171 | */ 172 | static from( 173 | fn: () => Result | PromiseLike>, 174 | ): Task { 175 | const p = new Promise>((resolve) => resolve(fn())) 176 | .catch(asInfallible); 177 | 178 | return new Task((resolve) => resolve(p)); 179 | } 180 | 181 | /** 182 | * Use this to create a `Task` from a `Promise`. 183 | * 184 | * You have to provide an `errorMapFn` in case the promise rejects, so that 185 | * the type can be inferred. 186 | * 187 | * If you are certain(!) that the provided promise will never reject, you can 188 | * provide the {@linkcode asInfallible} helper from the core module. 189 | * 190 | * @category Task::Basic 191 | * 192 | * @example 193 | * ```typescript 194 | * import { asInfallible, Task } from "https://deno.land/x/eitherway/mod.ts"; 195 | * 196 | * const willBeString = new Promise((resolve) => { 197 | * setTimeout(() => resolve("42"), 500); 198 | * }); 199 | * 200 | * const task: Task = Task.fromPromise( 201 | * willBeString, 202 | * asInfallible, 203 | * ); 204 | * ``` 205 | */ 206 | static fromPromise( 207 | promise: Promise, 208 | errorMapFn: (reason: unknown) => E, 209 | ): Task { 210 | return Task.fromFallible(() => promise, errorMapFn); 211 | } 212 | 213 | /** 214 | * Use this to construct a `Task` from the return value of a fallible 215 | * function. 216 | * 217 | * @category Task::Basic 218 | * 219 | * @example 220 | * ```typescript 221 | * import { Task } from "https://deno.land/x/eitherway/mod.ts"; 222 | * 223 | * async function rand(): Promise { 224 | * throw new TypeError("Oops"); 225 | * } 226 | * 227 | * function toTypeError(e: unknown): TypeError { 228 | * if (e instanceof TypeError) return e; 229 | * return TypeError("Unexpected error", { cause: e }); 230 | * } 231 | * 232 | * const task: Task = Task.fromFallible( 233 | * rand, 234 | * toTypeError, 235 | * ) 236 | * ``` 237 | */ 238 | static fromFallible( 239 | fn: () => T | PromiseLike, 240 | errorMapFn: (reason: unknown) => E, 241 | ): Task { 242 | const p = new Promise((resolve) => resolve(fn())) 243 | .then((v) => Ok(v), (e) => Err(errorMapFn(e))); 244 | 245 | return new Task((resolve) => resolve(p)); 246 | } 247 | 248 | /** 249 | * Use this lift a function into a `Task` context, by composing the wrapped 250 | * function with a `Result` constructor and an error mapping function. 251 | * 252 | * If no constructor is provided, `Ok` is used as a default. 253 | * 254 | * This higher order function is especially useful to intergrate 3rd party 255 | * code into your `Task` pipelines. 256 | * 257 | * @category Task::Advanced 258 | * 259 | * @example 260 | * ``` 261 | * import { Err, Ok, Result, Task } from "https://deno.land/x/eitherway/mod.ts"; 262 | * 263 | * async function toSpecialString(s: string): Promise { 264 | * if (s.length % 3 === 0) return s; 265 | * throw TypeError("Not confomrming to schema"); 266 | * } 267 | * 268 | * function toTypeError(e: unknown): TypeError { 269 | * if (e instanceof TypeError) return e; 270 | * return TypeError("Unexpected error", { cause: e }); 271 | * } 272 | * 273 | * const lifted = Task.liftFallible(toSpecialString, toTypeError); 274 | * 275 | * const task: Task = Task.succeed("abcd").andThen(lifted); 276 | * ``` 277 | */ 278 | static liftFallible( 279 | fn: (...args: Args) => R | PromiseLike, 280 | errorMapFn: (reason: unknown) => E, 281 | ctor: (arg: R) => Result | PromiseLike> = Ok as ( 282 | arg: R, 283 | ) => Result, 284 | ): (...args: Args) => Task { 285 | return function (...args: Args) { 286 | const p = new Promise((resolve) => resolve(fn(...args))) 287 | .then((v) => ctor(v), (e) => Err(errorMapFn(e))); 288 | 289 | return new Task((resolve) => resolve(p)); 290 | }; 291 | } 292 | 293 | /** 294 | * ====================== 295 | * TASK INSTANCE METHODS 296 | * ====================== 297 | */ 298 | 299 | /** 300 | * Use this to return the `Task` itself. Canonical identity function. 301 | * 302 | * Mostly useful for flattening or en lieu of a noop. 303 | * 304 | * This is mostly provided for compatibility with with `Result`. 305 | * 306 | * @category Task::Basic 307 | */ 308 | id(): Task { 309 | return this; 310 | } 311 | 312 | clone(): Task { 313 | return Task.of(cloneTask(this)); 314 | } 315 | 316 | map(mapFn: (v: T) => T2 | PromiseLike): Task { 317 | return Task.of(mapTaskSuccess(this, mapFn)); 318 | } 319 | 320 | mapOr( 321 | mapFn: (v: T) => T2 | PromiseLike, 322 | orValue: T2 | PromiseLike, 323 | ): Task { 324 | return Task.of(mapTaskSuccessOr(this, mapFn, orValue)); 325 | } 326 | 327 | mapOrElse( 328 | mapFn: (v: T) => T2 | PromiseLike, 329 | orFn: (e: E) => T2 | PromiseLike, 330 | ): Task { 331 | return Task.of(mapTaskSuccessOrElse(this, mapFn, orFn)); 332 | } 333 | 334 | mapErr(mapFn: (v: E) => E2 | PromiseLike): Task { 335 | return Task.of(mapTaskFailure(this, mapFn)); 336 | } 337 | 338 | andThen( 339 | thenFn: (v: T) => Result | PromiseLike>, 340 | ): Task { 341 | return Task.of(chainTaskSuccess(this, thenFn)); 342 | } 343 | 344 | orElse( 345 | elseFn: (v: E) => Result | PromiseLike>, 346 | ): Task { 347 | return Task.of(chainTaskFailure(this, elseFn)); 348 | } 349 | 350 | /** 351 | * Use this to conditionally pass-through the encapsulated value `` 352 | * based upon the outcome of the supplied `ensureFn`. 353 | * 354 | * In case of `Err`, this method short-circuits. 355 | * 356 | * In case of `Ok`, the supplied `ensureFn` is called with the encapsulated 357 | * value `` and if the return value is: 358 | * - `Ok`: it is discarded and the original `Ok` is returned 359 | * - `Err`: `Err` is returned 360 | * 361 | * See {@linkcode Task#orEnsure} for the opposite case. 362 | * 363 | * This is equivalent to chaining: 364 | * `original.andThen(ensureFn).and(original)` 365 | * 366 | * |**LHS andEnsure RHS**|**RHS: Ok**|**RHS: Err**| 367 | * |:-------------------:|:-------------:|:--------------:| 368 | * | **LHS: Ok** | Ok | Err | 369 | * | **LHS: Err** | Err | Err | 370 | * 371 | * @category Task::Advanced 372 | * 373 | * @example 374 | * ```typescript 375 | * import { Task } from "./task.ts"; 376 | * 377 | * declare function getPath(): Task; 378 | * declare function isReadableDir(path: string): Task; 379 | * declare function getFileExtensions(path: string): Task; 380 | * 381 | * getPath() 382 | * .andEnsure(isReadableDir) 383 | * .andThen(getFileExtensions) 384 | * .inspect((exts: string[]) => console.log(exts)) 385 | * .inspectErr((err: Error | TypeError) => console.log(err)) 386 | * ``` 387 | */ 388 | andEnsure( 389 | ensureFn: (v: T) => Result | PromiseLike>, 390 | ): Task { 391 | return Task.of(andEnsureTask(this, ensureFn)); 392 | } 393 | 394 | /** 395 | * Use this to conditionally pass-through the encapsulated value `` 396 | * based upon the outcome of the supplied `ensureFn`. 397 | * 398 | * In case of `Ok`, this method short-circuits. 399 | * 400 | * In case of `Err`, the supplied `ensureFn` is called with the encapsulated 401 | * value `` and if the return value is: 402 | * - `Ok`: it is returned 403 | * - `Err`: it is discarded and the original `Err` is returned 404 | * 405 | * See {@linkcode Task#andEnsure} for the opposite case. 406 | * 407 | * This is equivalent to chaining: 408 | * `original.orElse(ensureFn).or(original)` 409 | * 410 | * |**LHS orEnsure RHS**|**RHS: Ok**|**RHS: Err**| 411 | * |:------------------:|:-------------:|:--------------:| 412 | * | **LHS: Ok** | Ok | Ok | 413 | * | **LHS: Err** | Ok | Err | 414 | * 415 | * @category Task::Advanced 416 | * 417 | * @example 418 | * ```typescript 419 | * import { Task } from "./task.ts"; 420 | * 421 | * declare function getConfig(): Task; 422 | * declare function getFallback(err: RangeError): Task; 423 | * declare function configureService(path: string): Task; 424 | * 425 | * getConfig() 426 | * .orEnsure(getFallback) 427 | * .andThen(configureService) 428 | * .inspect((_: void) => console.log("Done!")) 429 | * .inspectErr((err: RangeError | TypeError) => console.log(err)) 430 | * ``` 431 | */ 432 | orEnsure( 433 | ensureFn: (v: E) => Result | PromiseLike>, 434 | ): Task { 435 | return Task.of(orEnsureTask(this, ensureFn)); 436 | } 437 | 438 | zip( 439 | rhs: Result | PromiseLike>, 440 | ): Task<[T, T2], E | E2> { 441 | return Task.of(zipTask(this, rhs)); 442 | } 443 | 444 | tap(tapFn: (v: Result) => void | PromiseLike): Task { 445 | return Task.of(tapTask(this, tapFn)); 446 | } 447 | 448 | inspect(inspectFn: (v: T) => void | PromiseLike): Task { 449 | return Task.of(inspectTaskSuccess(this, inspectFn)); 450 | } 451 | 452 | inspectErr(inspectFn: (v: E) => void | PromiseLike): Task { 453 | return Task.of(inspectTaskFailure(this, inspectFn)); 454 | } 455 | 456 | /** 457 | * @deprecated (will be removed in 1.0.0) use {@linkcode Task#andEnsure} instead 458 | */ 459 | trip( 460 | tripFn: (v: T) => Result | PromiseLike>, 461 | ): Task { 462 | return Task.of(andEnsureTask(this, tripFn)); 463 | } 464 | 465 | /** 466 | * @deprecated (will be removed in 1.0.0) use {@linkcode Task#orEnsure} instead 467 | */ 468 | rise( 469 | riseFn: (v: E) => Result | PromiseLike>, 470 | ): Task { 471 | return Task.of(orEnsureTask(this, riseFn)); 472 | } 473 | 474 | /** 475 | * Use this to get the wrapped value out of an `Task` instance 476 | * 477 | * Returns the wrapped value of type `` in case of `Ok` OR 478 | * `` in case of `Err`. 479 | * 480 | * In contrast to other implementations, this method NEVER throws an 481 | * exception 482 | * 483 | * @category Task::Basic 484 | * 485 | * @example 486 | * ```typescript 487 | * import { assert } from "../core/assert.ts"; 488 | * import { Result } from "../core/result.ts"; 489 | * import { Task } from "./task.ts"; 490 | * 491 | * const ok = Result(42) as Result; 492 | * const task = Task.of(ok); 493 | * 494 | * const union: number | string = await task.unwrap(); 495 | * 496 | * assert(union === 42); 497 | * ``` 498 | */ 499 | unwrap(): Promise { 500 | return unwrapTask(this); 501 | } 502 | 503 | /** 504 | * Same as {@linkcode Task#unwrap} but returns a default value in case the 505 | * underlying `Result` is an `Err` 506 | * 507 | * @category Task::Basic 508 | * 509 | * @example 510 | * ```typescript 511 | * import { assert } from "../core/assert.ts"; 512 | * import { Result } from "../core/result.ts"; 513 | * import { Task } from "./task.ts"; 514 | * 515 | * const err = Result(Error()) as Result; 516 | * const task = Task.of(err); 517 | * 518 | * const union: number | string = await task.unwrapOr(Promise.resolve("foo")); 519 | * 520 | * assert(union === "foo"); 521 | * ``` 522 | */ 523 | unwrapOr(orValue: T2 | PromiseLike): Promise { 524 | return unwrapTaskOr(this, orValue); 525 | } 526 | 527 | /** 528 | * Same as {@linkcode Task#unwrap} but returns a fallback value, which can based 529 | * constructed from the underlying value of type `` in case of `Err` 530 | * 531 | * @category Task::Basic 532 | * 533 | * @example 534 | * ```typescript 535 | * import { assert } from "../core/assert.ts"; 536 | * import { Result } from "../core/result.ts"; 537 | * import { Task } from "./task.ts"; 538 | * 539 | * const err = Result(Error("foo")) as Result; 540 | * const task = Task.of(err); 541 | * 542 | * const union: number | string = await task.unwrapOrElse( 543 | * async (err) => err.message 544 | * ); 545 | * 546 | * assert(union === "foo"); 547 | * ``` 548 | */ 549 | unwrapOrElse(orFn: (e: E) => T2 | PromiseLike): Promise { 550 | return unwrapTaskOrElse(this, orFn); 551 | } 552 | 553 | /** 554 | * Use this to obtain an async iterator of the encapsulated value `` 555 | * 556 | * In case of failure, this method returns the empty `AsyncIteratorResult` 557 | * 558 | * @category Task::Advanced 559 | * 560 | * @example 561 | * ```typescript 562 | * import { assert } from "../core/assert.ts" 563 | * import { Err, Ok, Result, Task } from "https://deno.land/x/eitherway/mod.ts"; 564 | * 565 | * const success = Task.succeed(42); 566 | * const failure = Task.fail(Error()); 567 | * 568 | * async function main() { 569 | * const okIter = success.iter(); 570 | * const errIter = failure.iter(); 571 | * 572 | * let okCount = 0; 573 | * let okYieldedValue = undefined; 574 | * 575 | * for await (const v of okIter) { 576 | * okCount += 1; 577 | * okYieldedValue = v; 578 | * } 579 | * 580 | * let errCount = 0; 581 | * let errYieldedValue = undefined; 582 | * 583 | * for await (const v of errIter) { 584 | * errCount += 1; 585 | * errYieldedValue = v; 586 | * } 587 | * 588 | * assert(okCount === 1); 589 | * assert(okYieldedValue === 42); 590 | * assert(errCount === 0) 591 | * assert(errYieldedValue === undefined); 592 | * } 593 | * 594 | * main().then(() => console.log("Done")); 595 | * ``` 596 | */ 597 | iter(): AsyncIterableIterator { 598 | return iterTask(this); 599 | } 600 | 601 | /** 602 | * ============================ 603 | * WELL-KNOWN SYMBOLS & METHODS 604 | * ============================ 605 | */ 606 | 607 | /** 608 | * Use this to get the full string tag 609 | * Short-hand for `Object.prototype.toString.call(task)` 610 | * 611 | * See the [reference](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/toString) 612 | * 613 | * @category Result::Advanced 614 | * 615 | * @example 616 | * ```typescript 617 | * import { assert } from "../core/assert.ts"; 618 | * import { Task } from "./task.ts" 619 | * 620 | * const tag = Task.succeed(42).toString(); 621 | * 622 | * assert(tag === "[object eitherway::Task]"); 623 | * ``` 624 | */ 625 | toString(): string { 626 | return Object.prototype.toString.call(this); 627 | } 628 | 629 | /** 630 | * This well-known symbol is called by `Object.prototype.toString` to 631 | * obtain a string representation of a value's type 632 | * 633 | * This maybe useful for debugging or certain logs 634 | * 635 | * The [`.toString()`]{@link this#toString} method is a useful short-hand in these scenarios 636 | * 637 | * See the [reference](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol/toStringTag) 638 | * 639 | * @category Task::Advanced 640 | * 641 | * @example 642 | * ```typescript 643 | * import { assert } from "../core/assert.ts"; 644 | * import { Task } from "./task.ts" 645 | * 646 | * const task = Task.succeed({ a: 1, b: 2 }); 647 | * 648 | * const toString = Object.prototype.toString; 649 | * 650 | * assert(toString.call(task) === "[object eitherway::Task]"); 651 | * assert(toString.call(Task) === "[object eitherway::Task]"); 652 | * ``` 653 | */ 654 | get [Symbol.toStringTag](): string { 655 | return "eitherway::Task"; 656 | } 657 | static get [Symbol.toStringTag](): string { 658 | return "eitherway::Task"; 659 | } 660 | 661 | /** 662 | * In case of success AND that the encapsulated value `` implements the 663 | * async iterator protocol, this delegates to the underlying implementation 664 | * 665 | * In all other cases, it yields the empty `AsyncIteratorResult` 666 | * 667 | * @category Task::Advanced 668 | */ 669 | async *[Symbol.asyncIterator](): AsyncIterableIterator< 670 | T extends AsyncIterable ? U : never 671 | > { 672 | const res = await this; 673 | 674 | if (res.isErr()) return; 675 | 676 | const target = Object(res.unwrap()); 677 | 678 | if (!target[Symbol.asyncIterator]) return; 679 | 680 | yield* target; 681 | } 682 | } 683 | -------------------------------------------------------------------------------- /lib/core/option_type_test.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | InferredOptionType, 3 | InferredSomeTuple, 4 | InferredSomeType, 5 | } from "./option.ts"; 6 | import { None, Option, Options, Some } from "./option.ts"; 7 | import type { NonNullish } from "./type_utils.ts"; 8 | import { 9 | AssertFalse, 10 | assertStrictEquals, 11 | AssertTrue, 12 | assertType, 13 | Has, 14 | IsExact, 15 | IsNullable, 16 | } from "../../dev_deps.ts"; 17 | 18 | type IsOption = O extends Option ? true : false; 19 | type OptionType = O extends Readonly> ? T : never; 20 | type SomeType = S extends Readonly> ? T : never; 21 | 22 | Deno.test("eitherway::Option::TypeHelpers::TypeTests", async (t) => { 23 | await t.step( 24 | "InferredOptionTypes -> Inferres T[] from Option[]", 25 | () => { 26 | type StrictTuple = Readonly<[string, number, boolean]>; 27 | const correctTuple = [ 28 | Option("some" as string), 29 | Some(1 as number), 30 | Option(true as boolean), 31 | ] as const; 32 | 33 | assertType< 34 | IsExact, StrictTuple> 35 | >(true); 36 | }, 37 | ); 38 | 39 | await t.step("InferredSomeType -> Inferres T from Option", () => { 40 | const opt = Option(123); 41 | type StrictOption = Readonly>; 42 | type NormalOption = Option>; 43 | 44 | assertType, number[]>>(true); 45 | assertType, Record>>( 46 | true, 47 | ); 48 | assertType, number>>(true); 49 | }); 50 | 51 | await t.step("InferredOptionType -> Inferres Option from union", () => { 52 | const thenFn = (b: boolean) => { 53 | if (b) return Option("abc"); 54 | return Option(["a", "b", "c"]); 55 | }; 56 | 57 | type t = InferredOptionType>; 58 | 59 | assertType< 60 | Has< 61 | InferredOptionType>, 62 | Option 63 | > 64 | >(true); 65 | }); 66 | }); 67 | 68 | Deno.test("eitherway::Option::TypeTests", async (t) => { 69 | await t.step( 70 | "Option.from() -> Nullish types stripped from inner return type", 71 | () => { 72 | const input: string | undefined | null = "abc"; 73 | const option = Option.from(input); 74 | 75 | assertType>>(false); 76 | assertStrictEquals(option.isSome(), true); 77 | }, 78 | ); 79 | 80 | await t.step( 81 | "Option.fromFallible() -> Nullish and Error types stripped from inner return type", 82 | () => { 83 | const input: string | null | undefined | Error = "abc"; 84 | const option = Option.fromFallible(input); 85 | 86 | type returnTypeIsInfallable = AssertTrue< 87 | IsExact> 88 | >; 89 | assertStrictEquals(option.isSome(), true); 90 | }, 91 | ); 92 | 93 | await t.step( 94 | "Option.fromCoercible() -> Falsy types stripped from inner return type", 95 | () => { 96 | const input: number | "" | false | 0 = NaN; 97 | const option = Option.fromCoercible(input); 98 | 99 | type returnTypeIsTruthy = AssertTrue< 100 | IsExact> 101 | >; 102 | assertStrictEquals(option.isSome(), false); 103 | }, 104 | ); 105 | 106 | await t.step( 107 | "Option.fromCoercible() -> Falsy types stripped from inner union return type", 108 | () => { 109 | type LiteralUnion = "a" | "b" | "c" | "" | 0; 110 | const input: LiteralUnion = "a"; 111 | const option = Option.fromCoercible(input as LiteralUnion); 112 | 113 | type returnTypePreservesTruthyUnion = AssertTrue< 114 | IsExact> 115 | >; 116 | 117 | assertStrictEquals(option.isSome(), true); 118 | }, 119 | ); 120 | 121 | await t.step( 122 | "Option.identity() -> identity type is correctly inferred", 123 | () => { 124 | const opt = Option("some"); 125 | const nested = Option(opt); 126 | const strict = Option("some") as Readonly>; 127 | 128 | const identity = Option.id(opt); 129 | const nestedIdentity = Option.id(nested); 130 | const strictIdentity = Option.id(strict); 131 | 132 | assertType>>(true); 133 | assertType>>>(true); 134 | assertType>>(true); 135 | }, 136 | ); 137 | 138 | await t.step("Option.lift() -> Fn type is correctly inferred", () => { 139 | const isNonZeroInt = (n: number): boolean | undefined => { 140 | if (Number.isSafeInteger(n) && n > 0) return true; 141 | return false; 142 | }; 143 | 144 | const isNotFortyTwo = function ( 145 | n: number, 146 | ): boolean | undefined | Error | TypeError { 147 | if (n === 42) return Error("Cannot be 42"); 148 | return isNonZeroInt(n); 149 | }; 150 | 151 | const lifted = Option.lift(isNonZeroInt); 152 | const liftedWithCoercible = Option.lift(isNonZeroInt, Option.fromCoercible); 153 | const liftedWithFallible = Option.lift(isNotFortyTwo, Option.fromFallible); 154 | 155 | assertType Option>>(true); 156 | assertType< 157 | IsExact Option> 158 | >(true); 159 | assertType< 160 | IsExact Option> 161 | >(true); 162 | }); 163 | 164 | await t.step( 165 | "Option.lift() -> Fn type is correctly inferred from custom Option ctor", 166 | () => { 167 | type Left = { tag: "Left"; value: L }; 168 | type Right = { tag: "Right"; value: R }; 169 | type Either = Left | Right; 170 | type Numeric = T extends number | bigint ? T : never; 171 | type NonNumeric = NonNullish>>; 172 | function isNonNumeric(arg: T): arg is NonNumeric { 173 | if (arg == null || typeof arg === "number" || typeof arg === "bigint") { 174 | return false; 175 | } 176 | return true; 177 | } 178 | 179 | function fromEither( 180 | e?: Either, 181 | ): Option> { 182 | if (e?.tag === "Right" && isNonNumeric(e?.value)) return Some(e.value); 183 | return None; 184 | } 185 | 186 | function tupleToEither( 187 | arg: Readonly<[string, number | boolean]>, 188 | ): Either { 189 | return { tag: "Right", value: arg[1] }; 190 | } 191 | 192 | const lifted = Option.lift(tupleToEither, fromEither); 193 | 194 | assertType< 195 | IsExact, Parameters> 196 | >(true); 197 | assertType< 198 | IsExact< 199 | ReturnType, 200 | ReturnType> 201 | > 202 | >(true); 203 | }, 204 | ); 205 | }); 206 | 207 | Deno.test("eitherway::Options::TypeTests", async (t) => { 208 | await t.step( 209 | ".all() -> Heterogenous tuple types are correctly inferred", 210 | () => { 211 | type TestTuple = Readonly<[string, number, { a: number[] }]>; 212 | const optionTuple = [ 213 | Option("abc" as string), 214 | Option(100 as number), 215 | Option({ a: [] } as { a: number[] }), 216 | ] as const; 217 | const someTuple = [ 218 | Some("abc" as string), 219 | Some(100 as number), 220 | Some({ a: [] } as { a: number[] }), 221 | ] as const; 222 | 223 | const collectedOpts = Options.all(optionTuple); 224 | const collectedSomes = Options.all(someTuple); 225 | 226 | if (collectedOpts.isSome() && collectedSomes.isSome()) { 227 | const unwrappedOpts = collectedOpts.unwrap(); 228 | const unwrappedSomes = collectedSomes.unwrap(); 229 | 230 | assertType>(true); 231 | assertType>(true); 232 | } else { 233 | throw TypeError("Unreachable"); 234 | } 235 | }, 236 | ); 237 | 238 | await t.step( 239 | ".all() -> Array types are correctly inferred and retain constraints", 240 | () => { 241 | type TestArray = ReadonlyArray; 242 | type TestArrayMut = Array; 243 | const optArray: ReadonlyArray> = Array.of(..."option").map( 244 | ( 245 | char, 246 | ) => Option(char), 247 | ); 248 | const optArrayMut: Array> = Array.of(..."option").map(( 249 | char, 250 | ) => Option(char)); 251 | 252 | const collected = Options.all(optArray); 253 | const collectedMut = Options.all(optArrayMut); 254 | 255 | if (collected.isSome() && collectedMut.isSome()) { 256 | const unwrapped = collected.unwrap(); 257 | const unwrappedMut = collectedMut.unwrap(); 258 | 259 | assertType>(true); 260 | assertType>(true); 261 | } else { 262 | throw TypeError("Unreachable"); 263 | } 264 | }, 265 | ); 266 | }); 267 | 268 | Deno.test("eitherway::Option::Some::TypeTests", async (t) => { 269 | await t.step("Some -> Ctor doesn't accept Nullish types", () => { 270 | type NullishUnion = string | undefined | null; 271 | 272 | /** 273 | * These examples don't compile: 274 | * 275 | * const nullish = undefined; 276 | * const maybeNullish = (): NullishUnion => undefined; 277 | * 278 | * const notSome = Some(nullish); 279 | * ^^^^^^^ 280 | * const tryMaybe = Some(maybeNullish()); 281 | * ^^^^^^^^^^^^^^ 282 | */ 283 | 284 | const input = "abc"; 285 | const some = Some(input); 286 | 287 | type ParameterTypeIsNotNullable = AssertFalse< 288 | IsNullable> 289 | >; 290 | type ParameterTypeCannotBeNullishUnion = AssertFalse< 291 | Has, NullishUnion> 292 | >; 293 | 294 | assertStrictEquals(some.isSome(), true); 295 | }); 296 | 297 | await t.step("Some -> Type predicate narrows to Some", () => { 298 | const opt = Option.from("abc" as string | undefined); 299 | 300 | if (opt.isSome()) { 301 | const str = opt.unwrap(); 302 | 303 | type IsString = AssertTrue>; 304 | } 305 | 306 | const union = opt.unwrap(); 307 | 308 | type IsUnion = AssertTrue>; 309 | 310 | assertStrictEquals(opt.isSome(), true); 311 | }); 312 | 313 | await t.step("Some -> Logical Combinators (&&, ||, ^)", async (t) => { 314 | await t.step(".and() -> Return type is inferred from RHS", () => { 315 | const lhs = Some("abc"); 316 | const rhs = Some(123); 317 | const res = lhs.and(rhs); 318 | 319 | type IsInferredFromRhs = AssertTrue< 320 | IsExact, SomeType> 321 | >; 322 | 323 | assertStrictEquals(rhs.unwrap(), res.unwrap()); 324 | }); 325 | await t.step(".or() -> Return type is inferred from LHS", () => { 326 | const lhs = Some("abc"); 327 | const rhs = Some(123); 328 | const res = lhs.or(rhs); 329 | 330 | type IsInferredFromLhs = AssertTrue< 331 | IsExact, SomeType> 332 | >; 333 | 334 | assertStrictEquals(lhs.unwrap(), res.unwrap()); 335 | }); 336 | await t.step(".xor() -> Return type is inferred from LHS or union", () => { 337 | const lhs = Some("abc"); 338 | const lhs2 = Option.from("abc"); 339 | const rhs = Option.from(123); 340 | const res = lhs.xor(rhs); 341 | const res2 = lhs2.xor(rhs); 342 | 343 | type IsInferredFromLhs = AssertTrue< 344 | IsExact, SomeType> 345 | >; 346 | type InnerIsInferredAsUnion = AssertTrue< 347 | IsExact, string | number> 348 | >; 349 | type IsInferredAsUnion = AssertTrue< 350 | IsExact 351 | >; 352 | 353 | assertStrictEquals(res.isNone(), true); 354 | }); 355 | await t.step( 356 | ".and().or().xor() -> Return type from chaining is inferred as union", 357 | () => { 358 | const lhs = Some({ a: 1, b: 2 }); 359 | const rhs = Some([1, 2, 3]); 360 | const rhs2 = Some("abc"); 361 | const rhs3 = Option.from(null); 362 | const rhs4 = Some(true); 363 | const rhs5 = Some(123); 364 | const res = lhs.and(rhs).or(rhs2).xor(rhs3).and(rhs4).xor(rhs5); 365 | 366 | type InnerIsInferredAsUnion = AssertTrue< 367 | IsExact, boolean | number> 368 | >; 369 | type IsInferredAsUnion = AssertTrue< 370 | IsExact | Option> 371 | >; 372 | 373 | assertStrictEquals(res.isNone(), true); 374 | }, 375 | ); 376 | }); 377 | 378 | await t.step("Some -> Map Methods", async (t) => { 379 | await t.step( 380 | ".map() -> Return type is invariant over Nullish mapFn return types", 381 | () => { 382 | const mapFn = (n: number): string => n < 100 ? "lt" : "gte"; 383 | const some = Some(99); 384 | const someStr = some.map(mapFn); 385 | 386 | type MapMethodReturnType = ReturnType; 387 | 388 | type IsInvariantOverNullish = AssertFalse< 389 | IsNullable> 390 | >; 391 | 392 | /** 393 | * Example: This doesn't compile 394 | * const mapFn = (n: number): string | undefined => n < 100 : "less" : undefined; 395 | * const fails = Some(99).map(mapFn); 396 | * ^^^^^ 397 | */ 398 | 399 | assertStrictEquals(someStr.unwrap(), "lt"); 400 | }, 401 | ); 402 | await t.step( 403 | ".mapOr() -> Return type is invariant over Nullish mapFn return and orValue types", 404 | () => { 405 | const mapFn = (n: number): string => n < 100 ? "lt" : "gte"; 406 | const some = Some(99); 407 | const someStr = some.mapOr(mapFn, "never"); 408 | 409 | type MapOrMethodReturnType = ReturnType; 410 | 411 | type IsInvariantOverNullish = AssertFalse< 412 | IsNullable> 413 | >; 414 | 415 | /** 416 | * Example: This doesn't compile 417 | * const mapFn = (n: number): string | undefined => n < 100 : "less" : undefined; 418 | * const fails = Some(99).mapOr(mapFn, undefined); 419 | * ^^^^^ ^^^^^^^^^ 420 | */ 421 | 422 | assertStrictEquals(someStr.unwrap(), "lt"); 423 | }, 424 | ); 425 | await t.step( 426 | ".mapOrElse() -> Return type is invariant over Nullish mapFn return and orFn return types", 427 | () => { 428 | const mapFn = (n: number): string => n < 100 ? "lt" : "gte"; 429 | const orFn = () => "never"; 430 | const some = Some(99); 431 | const someStr = some.mapOrElse(mapFn, orFn); 432 | 433 | type MapOrElseMethodReturnType = ReturnType; 434 | 435 | type IsInvariantOverNullish = AssertFalse< 436 | IsNullable> 437 | >; 438 | 439 | /** 440 | * Example: This doesn't compile 441 | * const mapFn = (n: number): string | undefined => n < 100 : "less" : undefined; 442 | * const orFn = () => undefined; 443 | * const fails = Some(99).mapOrElse(mapFn, orFn); 444 | * ^^^^^ ^^^^^ 445 | */ 446 | 447 | assertStrictEquals(someStr.unwrap(), "lt"); 448 | }, 449 | ); 450 | await t.step( 451 | ".andThen() -> Return type is always Option (and thus invariant over Nullish)", 452 | () => { 453 | const mapFn = (n: number) => Math.max(n, 10); 454 | const thenFn = (n: number) => Option.fromCoercible(0 * n / 0.00); 455 | const some = Some(99); 456 | const res = some.map(mapFn).andThen(thenFn); 457 | 458 | type AndThenParamReturnType = ReturnType< 459 | Parameters[0] 460 | >; 461 | 462 | type ReturnTypeIsOption = AssertTrue>; 463 | type IsInvariantOverNullish = AssertFalse< 464 | IsNullable> 465 | >; 466 | 467 | assertStrictEquals(res.unwrap(), undefined); 468 | assertStrictEquals(res.isNone(), true); 469 | }, 470 | ); 471 | 472 | await t.step("filter() -> Return type is narrowed by typeguard", () => { 473 | const isNum = (value: unknown): value is number => 474 | typeof value === "number"; 475 | const numOrStr = 123 as number | string; 476 | const some = Some(numOrStr); 477 | 478 | const res = some.filter(isNum); 479 | 480 | type IsNarrowed = AssertTrue>>; 481 | 482 | assertStrictEquals(res.isSome(), true); 483 | }); 484 | 485 | await t.step("filter() -> Return type is unchanged by predicate", () => { 486 | const isEven = (value: number) => value % 2 === 0; 487 | const num = 123; 488 | const some = Some(num); 489 | 490 | const res = some.filter(isEven); 491 | 492 | assertType, number>>(true); 493 | assertStrictEquals(res.isNone(), true); 494 | }); 495 | }); 496 | 497 | await t.step("Some -> Unwrap Methods", async (t) => { 498 | await t.step( 499 | ".unwrap() -> Return type is T or Nullish union (T | undefined)", 500 | () => { 501 | const some = Some(123); 502 | const someRes = some.unwrap(); 503 | const opt = Option.from(123 as number | undefined); 504 | const optRes = opt.unwrap(); 505 | 506 | type UnwrappedSomeIsNotNullable = AssertFalse< 507 | IsNullable 508 | >; 509 | type UnwrappedOptionIsNullable = AssertTrue>; 510 | type UnwrappedOptionIsUnion = AssertTrue< 511 | IsExact 512 | >; 513 | 514 | assertStrictEquals(someRes, optRes); 515 | }, 516 | ); 517 | await t.step(".unwrapOr() -> Return type is T or union (T | U)", () => { 518 | const some = Some(123); 519 | const someRes = some.unwrapOr("123" as string); 520 | const opt = Option.from(123 as number | undefined); 521 | const optRes = opt.unwrapOr("123" as string); 522 | 523 | type UnwrappedSomeIsNotNullable = AssertFalse>; 524 | type UnwrappedSomeIsExact = AssertTrue>; 525 | type UnwrappedOptionIsNotNullable = AssertFalse< 526 | IsNullable 527 | >; 528 | type UnwrappedOptionIsUnion = AssertTrue< 529 | IsExact 530 | >; 531 | 532 | assertStrictEquals(someRes, optRes); 533 | }); 534 | await t.step(".unwrapOrElse() -> Return type is T or union (T | U)", () => { 535 | const some = Some(123); 536 | const someRes = some.unwrapOrElse(() => "123" as string); 537 | const opt = Option.from(123 as number | undefined); 538 | const optRes = opt.unwrapOrElse(() => "123" as string); 539 | 540 | type UnwrappedSomeIsNotNullable = AssertFalse>; 541 | type UnwrappedSomeIsExact = AssertTrue>; 542 | type UnwrappedOptionIsNotNullable = AssertFalse< 543 | IsNullable 544 | >; 545 | type UnwrappedOptionIsUnion = AssertTrue< 546 | IsExact 547 | >; 548 | 549 | assertStrictEquals(someRes, optRes); 550 | }); 551 | }); 552 | 553 | await t.step("Some - JS well-known Symbols and Methods", async (t) => { 554 | await t.step( 555 | "[Symbol.iterator]() -> Return type correctly inferes delegation", 556 | () => { 557 | class MyIterable implements Iterable { 558 | #value: number; 559 | constructor(value: number) { 560 | this.#value = value; 561 | } 562 | *[Symbol.iterator]() { 563 | yield this.#value; 564 | } 565 | } 566 | 567 | const arr = [1, 2, 3]; 568 | const str = "thing"; 569 | const num = 42; 570 | const iterable = new MyIterable(123); 571 | 572 | const someArr = Some(arr); 573 | const someStr = Some(str); 574 | const someNum = Some(num); 575 | const someIter = Some(iterable); 576 | 577 | const someArrIter = someArr[Symbol.iterator](); 578 | const someStrIter = someStr[Symbol.iterator](); 579 | const someNumIter = someNum[Symbol.iterator](); 580 | const someIterIter = someIter[Symbol.iterator](); 581 | 582 | assertType>>(true); 583 | assertType>>(true); 584 | assertType>>(true); 585 | assertType>>( 586 | true, 587 | ); 588 | }, 589 | ); 590 | }); 591 | }); 592 | 593 | Deno.test("eitherway::Option::None::TypeTests", async (t) => { 594 | await t.step("None -> Map Methods", async (t) => { 595 | await t.step(".map() -> Return type is None", () => { 596 | const none = None.map(() => "never"); 597 | 598 | type IsNone = AssertTrue>; 599 | 600 | assertStrictEquals(none.isNone(), true); 601 | }); 602 | 603 | await t.step( 604 | ".filter() -> Narrows type based on supplied typeguard", 605 | () => { 606 | const numOrStr = 0 as string | number; 607 | const isNum = (value: unknown): value is number => 608 | typeof value === "number"; 609 | 610 | const none = Option.fromCoercible(numOrStr); 611 | const same = none.filter(isNum); 612 | 613 | type PriorIsUnion = AssertTrue< 614 | IsExact> 615 | >; 616 | type ReturnTypeIsNarrowed = AssertTrue< 617 | IsExact> 618 | >; 619 | 620 | assertStrictEquals(same.isNone(), true); 621 | }, 622 | ); 623 | 624 | await t.step(".filter() -> Return type is None", () => { 625 | const isEven = (value: number): boolean => value % 2 === 0; 626 | 627 | const none = None.filter(isEven); 628 | 629 | type IsNone = AssertTrue>; 630 | 631 | assertStrictEquals(none.isNone(), true); 632 | }); 633 | }); 634 | }); 635 | -------------------------------------------------------------------------------- /lib/async/task_test.ts: -------------------------------------------------------------------------------- 1 | //deno-lint-ignore-file require-await 2 | import { asInfallible, Err, Ok, Result } from "../core/mod.ts"; 3 | import { Task } from "./task.ts"; 4 | import { 5 | assertInstanceOf, 6 | assertStrictEquals, 7 | assertType, 8 | } from "../../dev_deps.ts"; 9 | import type { Empty } from "../core/mod.ts"; 10 | import type { IsExact } from "../../dev_deps.ts"; 11 | 12 | Deno.test("eitherway::Task", async (t) => { 13 | await t.step("Task -> Constructors", async (t) => { 14 | await t.step( 15 | ".of() -> propagates exception asynchronously if Promise> throws", 16 | async () => { 17 | const SHOULD_THROW = true; 18 | const te = TypeError("Cannot do that"); 19 | const throws = async () => { 20 | if (SHOULD_THROW) throw te; 21 | return Ok(42); 22 | }; 23 | const promiseRes = throws(); 24 | 25 | const task = Task.of(promiseRes); 26 | 27 | task.catch((e) => assertStrictEquals(e, te)); 28 | }, 29 | ); 30 | 31 | await t.step(".deferred() -> creates a new deferred Task", async () => { 32 | class TimeoutError extends Error {} 33 | 34 | const { task, succeed, fail } = Task.deferred(); 35 | 36 | const successId = setTimeout(() => succeed(42), 100); 37 | const failureId = setTimeout(() => fail(new TimeoutError()), 10); 38 | 39 | const res = await task; 40 | 41 | assertStrictEquals(task instanceof Task, true); 42 | assertStrictEquals(res.isErr(), true); 43 | assertInstanceOf(res.unwrap(), TimeoutError); 44 | 45 | clearTimeout(successId); 46 | clearTimeout(failureId); 47 | }); 48 | 49 | await t.step( 50 | ".deferred() -> subsequent calls don't alter the state, once the task is resolved", 51 | async () => { 52 | const { task, succeed, fail } = Task.deferred(); 53 | 54 | succeed(1); 55 | succeed(2); 56 | fail("Fail!"); 57 | succeed(3); 58 | 59 | const res = await task; 60 | 61 | assertStrictEquals(res.isOk(), true); 62 | assertStrictEquals(res.unwrap(), 1); 63 | }, 64 | ); 65 | 66 | await t.step( 67 | ".from() -> propagates exception asynchronously if infallible function throws", 68 | async () => { 69 | const SHOULD_THROW = true; 70 | const te = TypeError("Cannot do that"); 71 | const throws = () => { 72 | if (SHOULD_THROW) throw te; 73 | return Ok(42); 74 | }; 75 | 76 | const shouldBeInfallible = () => Task.from(throws); 77 | 78 | shouldBeInfallible().catch((e) => assertStrictEquals(e?.cause, te)); 79 | }, 80 | ); 81 | 82 | await t.step( 83 | ".from() -> returns Task from function returning Promise>", 84 | async () => { 85 | const succeed = async () => Ok("Hooray!"); 86 | 87 | const success = await Task.from(succeed); 88 | 89 | assertStrictEquals(success.isOk(), true); 90 | assertStrictEquals(success.unwrap(), "Hooray!"); 91 | }, 92 | ); 93 | 94 | await t.step( 95 | ".fromPromise() -> returns an infallible Task if errMapFn produces never", 96 | async () => { 97 | const futureNumber = Promise.resolve(42); 98 | 99 | const infallible = await Task.fromPromise(futureNumber, asInfallible); 100 | 101 | assertType>>(true); 102 | assertStrictEquals(infallible.isErr(), false); 103 | }, 104 | ); 105 | 106 | await t.step( 107 | ".fromPromise() -> propagates exception asynchronously if infallible function throws", 108 | async () => { 109 | const te = TypeError("Cannot do that"); 110 | const futureTypeError = Promise.reject(te); 111 | 112 | const shouldProduceInfallible = () => 113 | Task.fromPromise(futureTypeError, asInfallible); 114 | 115 | shouldProduceInfallible().catch((e) => 116 | assertStrictEquals(e?.cause, te) 117 | ); 118 | }, 119 | ); 120 | 121 | await t.step( 122 | ".fromPromise() -> returns a Task, where E is inferred from the errMapFn", 123 | async () => { 124 | const te = TypeError("Cannot do that"); 125 | const futureTypeError = Promise.reject(te); 126 | const errMapFn = (e: unknown) => { 127 | if (e instanceof TypeError) return e; 128 | return TypeError("Can also not do that"); 129 | }; 130 | 131 | const failed = await Task.fromPromise(futureTypeError, errMapFn); 132 | 133 | assertType>>(true); 134 | assertStrictEquals(failed.unwrap(), te); 135 | }, 136 | ); 137 | 138 | await t.step( 139 | ".fromFallible() -> returns a Task, where E is inferred from the errMapFn", 140 | async () => { 141 | const te = TypeError("Cannot do that"); 142 | const produceTypeError = () => { 143 | throw te; 144 | }; 145 | const produceTypeErrorAsync = async () => { 146 | produceTypeError(); 147 | }; 148 | const errMapFn = (e: unknown) => { 149 | if (e instanceof TypeError) return e; 150 | return TypeError("Can also not do that"); 151 | }; 152 | 153 | const failed = await Task.fromFallible(produceTypeError, errMapFn); 154 | const alsoFailed = await Task.fromFallible( 155 | produceTypeErrorAsync, 156 | errMapFn, 157 | ); 158 | 159 | assertType>>(true); 160 | assertType>>(true); 161 | assertStrictEquals(failed.unwrap(), te); 162 | assertStrictEquals(alsoFailed.unwrap(), te); 163 | }, 164 | ); 165 | 166 | await t.step( 167 | ".fromFallible() -> propagates exception asynchronously if infallible function throws", 168 | async () => { 169 | const te = TypeError("Cannot do that"); 170 | 171 | const shouldProduceInfallible = () => 172 | Task.fromFallible(() => { 173 | throw te; 174 | }, asInfallible); 175 | 176 | const shouldProduceInfallibleAsync = () => 177 | Task.fromFallible(async () => { 178 | throw te; 179 | }, asInfallible); 180 | 181 | shouldProduceInfallible().catch((e) => 182 | assertStrictEquals(e?.cause, te) 183 | ); 184 | shouldProduceInfallibleAsync().catch((e) => 185 | assertStrictEquals(e?.cause, te) 186 | ); 187 | }, 188 | ); 189 | 190 | await t.step( 191 | ".liftFallible() -> composes functions and constructors correctly", 192 | async () => { 193 | async function toSpecialString(s: string): Promise { 194 | if (s.length % 3 === 0) return s; 195 | throw TypeError("Not confomrming to schema"); 196 | } 197 | 198 | function toNumber(str: string): Result { 199 | return Ok(str.length); 200 | } 201 | 202 | function toTypeError(e: unknown): TypeError { 203 | if (e instanceof TypeError) return e; 204 | return TypeError("Unexpected error", { cause: e }); 205 | } 206 | 207 | const lifted = Task.liftFallible( 208 | toSpecialString, 209 | toTypeError, 210 | toNumber, 211 | ); 212 | 213 | const task = Task.succeed("abc").andThen(lifted); 214 | 215 | const res = await task; 216 | 217 | assertType< 218 | IsExact, Parameters> 219 | >(true); 220 | assertType>>(true); 221 | assertStrictEquals(res.isOk(), true); 222 | assertStrictEquals(res.unwrap(), 3); 223 | }, 224 | ); 225 | 226 | await t.step( 227 | ".liftFallible() -> maps caught exceptions correctly", 228 | async () => { 229 | async function toSpecialString(s: string): Promise { 230 | if (s.length % 3 === 0) return s; 231 | throw TypeError("Not confomrming to schema"); 232 | } 233 | 234 | function toNumber(str: string): Result { 235 | return Ok(str.length); 236 | } 237 | 238 | function toTypeError(e: unknown): TypeError { 239 | if (e instanceof TypeError) return e; 240 | return TypeError("Unexpected error", { cause: e }); 241 | } 242 | 243 | const lifted = Task.liftFallible( 244 | toSpecialString, 245 | toTypeError, 246 | toNumber, 247 | ); 248 | 249 | const task = Task.succeed("abcd").andThen(lifted); 250 | 251 | const res = await task; 252 | 253 | assertType< 254 | IsExact, Parameters> 255 | >(true); 256 | assertType>>(true); 257 | assertStrictEquals(res.isErr(), true); 258 | assertInstanceOf(res.unwrap(), TypeError); 259 | }, 260 | ); 261 | }); 262 | 263 | await t.step("Task -> Instance Methods", async (t) => { 264 | await t.step("Task -> Map Methods", async (t) => { 265 | await t.step( 266 | ".map() -> returns new Task instance with applied mapFn to value", 267 | async () => { 268 | const task = Task.succeed(41); 269 | 270 | const mapped = task.map(async (x) => x + 1); 271 | const res = await mapped; 272 | 273 | assertType>>(true); 274 | assertStrictEquals(mapped instanceof Task, true); 275 | assertStrictEquals(mapped === task, false); 276 | assertStrictEquals(res.isOk(), true); 277 | assertStrictEquals(res.unwrap(), 42); 278 | }, 279 | ); 280 | 281 | await t.step( 282 | ".mapErr() -> returns new Task instance with applied mapFn to err", 283 | async () => { 284 | const e = TypeError("Cannot do that"); 285 | const task = Task.fail(e); 286 | 287 | const mapped = task.mapErr((e) => 288 | TypeError("Received error", { cause: e }) 289 | ); 290 | const res = await mapped; 291 | 292 | assertType>>(true); 293 | assertStrictEquals(mapped instanceof Task, true); 294 | assertStrictEquals(mapped === task, false); 295 | assertStrictEquals(res.isErr(), true); 296 | assertStrictEquals(res.unwrap().cause, e); 297 | }, 298 | ); 299 | 300 | await t.step( 301 | ".andThen() -> returns new Task instance with applied fn to value", 302 | async () => { 303 | const task = Task.succeed("1"); 304 | const safeParse = async function ( 305 | str: string, 306 | ): Promise> { 307 | const n = Number.parseInt(str); 308 | 309 | return Number.isNaN(n) ? Err(TypeError("Cannot parse")) : Ok(n); 310 | }; 311 | 312 | const chained = task.andThen(safeParse); 313 | const res = await chained; 314 | 315 | assertType>>(true); 316 | //@ts-expect-error Incompatible types but ref COULD be equal 317 | assertStrictEquals(task === chained, false); 318 | assertStrictEquals(res.isOk(), true); 319 | assertStrictEquals(res.unwrap(), 1); 320 | }, 321 | ); 322 | 323 | await t.step( 324 | ".orElse() -> returns a new Task instance with applied fn to err", 325 | async () => { 326 | const task = Task.fail( 327 | Error("Received error", { cause: TypeError("Cannot do that") }), 328 | ); 329 | const rehydrate = function (err: unknown): Task { 330 | if (!(err instanceof Error)) { 331 | return Task.fail(RangeError("Cannot rehydrate")); 332 | } 333 | return Task.succeed(0); 334 | }; 335 | 336 | const chained = task.orElse(rehydrate); 337 | const res = await chained; 338 | 339 | assertType>>(true); 340 | assertStrictEquals(task === chained, false); 341 | assertStrictEquals(res.isOk(), true); 342 | assertStrictEquals(res.unwrap(), 0); 343 | }, 344 | ); 345 | 346 | await t.step( 347 | ".trip() -> returns Ok value as is if tripFn succeeds", 348 | async () => { 349 | const success: Task = Task.succeed(42); 350 | const tripFn = (n: number): Result => 351 | n % 2 === 0 ? Ok.empty() : Err(RangeError()); 352 | 353 | const okTask = success.trip(tripFn); 354 | 355 | assertType< 356 | IsExact> 357 | >(true); 358 | 359 | const ok = await okTask; 360 | 361 | assertStrictEquals(ok.isOk(), true); 362 | assertStrictEquals(ok.unwrap(), 42); 363 | }, 364 | ); 365 | 366 | await t.step( 367 | ".trip() -> derails the successful Task if tripFn fails", 368 | async () => { 369 | const re = RangeError("Cannot do that"); 370 | const success: Task = Task.succeed(41); 371 | const tripFn = (n: number): Result => 372 | n % 2 === 0 ? Ok.empty() : Err(re); 373 | 374 | const okTask = success.trip(tripFn); 375 | 376 | assertType< 377 | IsExact> 378 | >(true); 379 | 380 | const ok = await okTask; 381 | 382 | assertStrictEquals(ok.isOk(), false); 383 | assertStrictEquals(ok.unwrap(), re); 384 | }, 385 | ); 386 | 387 | await t.step(".trip() -> is a no-op in case of Err", async () => { 388 | const te = TypeError("Cannot do that"); 389 | const failure: Task = Task.fail(te); 390 | const tripFn = (n: number): Result => 391 | n % 2 === 0 ? Ok.empty() : Err(RangeError()); 392 | 393 | const errTask = failure.trip(tripFn); 394 | 395 | assertType< 396 | IsExact> 397 | >(true); 398 | 399 | const err = await errTask; 400 | 401 | assertStrictEquals(err.isErr(), true); 402 | assertStrictEquals(err.unwrap(), te); 403 | }); 404 | 405 | await t.step( 406 | ".andEnsure() -> returns Ok value as is if ensureFn succeeds", 407 | async () => { 408 | const success: Task = Task.succeed(42); 409 | const ensureFn = (n: number): Result => 410 | n % 2 === 0 ? Ok.empty() : Err(RangeError()); 411 | 412 | const okTask = success.andEnsure(ensureFn); 413 | 414 | assertType< 415 | IsExact> 416 | >(true); 417 | 418 | const ok = await okTask; 419 | 420 | assertStrictEquals(ok.isOk(), true); 421 | assertStrictEquals(ok.unwrap(), 42); 422 | }, 423 | ); 424 | 425 | await t.step( 426 | ".andEnsure() -> derails the successful Task if ensureFn fails", 427 | async () => { 428 | const re = RangeError("Cannot do that"); 429 | const success: Task = Task.succeed(41); 430 | const ensureFn = (n: number): Result => 431 | n % 2 === 0 ? Ok.empty() : Err(re); 432 | 433 | const okTask = success.andEnsure(ensureFn); 434 | 435 | assertType< 436 | IsExact> 437 | >(true); 438 | 439 | const ok = await okTask; 440 | 441 | assertStrictEquals(ok.isOk(), false); 442 | assertStrictEquals(ok.unwrap(), re); 443 | }, 444 | ); 445 | 446 | await t.step(".andEnsure() -> is a no-op in case of Err", async () => { 447 | const te = TypeError("Cannot do that"); 448 | const failure: Task = Task.fail(te); 449 | const ensureFn = (n: number): Result => 450 | n % 2 === 0 ? Ok.empty() : Err(RangeError()); 451 | 452 | const errTask = failure.andEnsure(ensureFn); 453 | 454 | assertType< 455 | IsExact> 456 | >(true); 457 | 458 | const err = await errTask; 459 | 460 | assertStrictEquals(err.isErr(), true); 461 | assertStrictEquals(err.unwrap(), te); 462 | }); 463 | 464 | await t.step( 465 | ".orEnsure() -> returns the original Err if ensureFn fails", 466 | async () => { 467 | const te = TypeError("Cannot do that"); 468 | const failure: Task = Task.fail(te); 469 | const ensureFn = (err: TypeError): Result => 470 | err.message.length % 2 !== 0 ? Ok.empty() : Err(RangeError()); 471 | 472 | const errTask = failure.orEnsure(ensureFn); 473 | 474 | assertType< 475 | IsExact> 476 | >(true); 477 | 478 | const err = await errTask; 479 | 480 | assertStrictEquals(err.isErr(), true); 481 | assertStrictEquals(err.unwrap(), te); 482 | }, 483 | ); 484 | 485 | await t.step( 486 | ".orEnsure() -> recovers the failed Task if ensureFn succeeds", 487 | async () => { 488 | const te = TypeError("Cannot do that"); 489 | const failure: Task = Task.fail(te); 490 | const ensureFn = (err: TypeError): Result => 491 | err.message.length % 2 === 0 ? Ok(42n) : Err(RangeError()); 492 | 493 | const okTask = failure.orEnsure(ensureFn); 494 | 495 | assertType< 496 | IsExact< 497 | typeof okTask, 498 | Task 499 | > 500 | >(true); 501 | 502 | const ok = await okTask; 503 | 504 | assertStrictEquals(ok.isOk(), true); 505 | assertStrictEquals(ok.unwrap(), 42n); 506 | }, 507 | ); 508 | 509 | await t.step(".orEnsure() -> is a no-op in case of Ok", async () => { 510 | const success: Task = Task.succeed(42); 511 | const ensureFn = (err: TypeError): Result => 512 | err instanceof TypeError ? Ok.empty() : Err(RangeError()); 513 | 514 | const okTask = success.orEnsure(ensureFn); 515 | 516 | assertType< 517 | IsExact> 518 | >(true); 519 | 520 | const ok = await okTask; 521 | 522 | assertStrictEquals(ok.isOk(), true); 523 | assertStrictEquals(ok.unwrap(), 42); 524 | }); 525 | }); 526 | 527 | await t.step("Task -> Unwrap Methods", async (t) => { 528 | await t.step(".unwrap() -> returns the wrapped value", async () => { 529 | const task: Task = Task.succeed(42); 530 | 531 | const unwrapped = task.unwrap(); 532 | const value = await unwrapped; 533 | 534 | assertType>>( 535 | true, 536 | ); 537 | assertStrictEquals(value, 42); 538 | }); 539 | 540 | await t.step( 541 | ".unwrapOr() -> returns the fallback value in case of Err", 542 | async () => { 543 | const task: Task = Task.fail(TypeError()); 544 | 545 | const unwrapped = task.unwrapOr("foo"); 546 | const value = await unwrapped; 547 | 548 | assertType>>(true); 549 | assertStrictEquals(value, "foo"); 550 | }, 551 | ); 552 | 553 | await t.step( 554 | ".unwrapOrElse() -> returns the constructed fallback value in case of Err", 555 | async () => { 556 | const task: Task = Task.fail(TypeError("foo")); 557 | 558 | const unwrapped = task.unwrapOrElse(async (err) => err.message); 559 | const value = await unwrapped; 560 | 561 | assertType>>(true); 562 | assertStrictEquals(value, "foo"); 563 | }, 564 | ); 565 | }); 566 | 567 | await t.step("Task -> Transformation Methods", async (t) => { 568 | await t.step( 569 | ".iter() -> returns an AsyncIteratable in case of success", 570 | async () => { 571 | const success: Task = Task.succeed("foo"); 572 | 573 | const okIter = success.iter(); 574 | 575 | let count = 0; 576 | let value: string | undefined; 577 | 578 | for await (const v of okIter) { 579 | count += 1; 580 | value = v; 581 | } 582 | 583 | assertType>>( 584 | true, 585 | ); 586 | assertStrictEquals(count, 1); 587 | assertStrictEquals(value, "foo"); 588 | }, 589 | ); 590 | 591 | await t.step( 592 | ".iter() -> returns an empty AsyncIteratable in case of failure", 593 | async () => { 594 | const failure: Task = Task.fail(Error()); 595 | 596 | const errIter = failure.iter(); 597 | 598 | let count = 0; 599 | let value: string | undefined; 600 | 601 | for await (const v of errIter) { 602 | count += 1; 603 | value = v; 604 | } 605 | 606 | assertType>>( 607 | true, 608 | ); 609 | assertStrictEquals(count, 0); 610 | assertStrictEquals(value, undefined); 611 | }, 612 | ); 613 | }); 614 | 615 | await t.step("Task -> JS well-known Symbols & Methods", async (t) => { 616 | await t.step(".toString() -> returns the full string tag", () => { 617 | const tag = Task.succeed(42).toString(); 618 | 619 | assertStrictEquals(tag, "[object eitherway::Task]"); 620 | }); 621 | 622 | await t.step("[Symbol.toStringTag] -> returns the FQN", () => { 623 | const fqn = Task.succeed(42)[Symbol.toStringTag]; 624 | 625 | assertStrictEquals(fqn, "eitherway::Task"); 626 | }); 627 | 628 | await t.step( 629 | "[Symbol.asyncIterator]() -> delegates to the underlying implementation in case of success", 630 | async () => { 631 | class FakeStream implements AsyncIterable { 632 | #value: number; 633 | constructor(value: number) { 634 | this.#value = value; 635 | } 636 | async *[Symbol.asyncIterator](): AsyncIterableIterator { 637 | yield this.#value; 638 | } 639 | } 640 | 641 | const stream = new FakeStream(42); 642 | const success: Task = Task.succeed(stream); 643 | 644 | let count = 0; 645 | let value: number | undefined; 646 | 647 | for await (const v of success) { 648 | count += 1; 649 | value = v; 650 | } 651 | 652 | const okIter = success[Symbol.asyncIterator](); 653 | 654 | assertType>>( 655 | true, 656 | ); 657 | assertStrictEquals(count, 1); 658 | assertStrictEquals(value, 42); 659 | }, 660 | ); 661 | 662 | await t.step( 663 | "[Symbol.asyncIterator]() -> returns an empty AsyncIterable if success value doesn't implement async iterator protocol", 664 | async () => { 665 | class FakeStream implements Iterable { 666 | #value: number; 667 | constructor(value: number) { 668 | this.#value = value; 669 | } 670 | *[Symbol.iterator](): IterableIterator { 671 | yield this.#value; 672 | } 673 | } 674 | 675 | const stream = new FakeStream(42); 676 | const success: Task = Task.succeed(stream); 677 | 678 | let count = 0; 679 | let value: number | undefined; 680 | 681 | for await (const v of success) { 682 | count += 1; 683 | value = v; 684 | } 685 | 686 | const okIter = success[Symbol.asyncIterator](); 687 | 688 | assertType>>( 689 | true, 690 | ); 691 | assertStrictEquals(count, 0); 692 | assertStrictEquals(value, undefined); 693 | }, 694 | ); 695 | 696 | await t.step( 697 | "[Symbol.asyncIterator]() -> returns an empty AsyncIterable in case of failure", 698 | async () => { 699 | const failure = Task.fail(Error()); 700 | 701 | let count = 0; 702 | let value: undefined; 703 | 704 | for await (const v of failure) { 705 | count += 1; 706 | value = v; 707 | } 708 | 709 | const errIter = failure[Symbol.asyncIterator](); 710 | 711 | assertType>>( 712 | true, 713 | ); 714 | assertStrictEquals(count, 0); 715 | assertStrictEquals(value, undefined); 716 | }, 717 | ); 718 | }); 719 | }); 720 | }); 721 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # eitherway 2 | 3 | [![maintainability](https://api.codeclimate.com/v1/badges/dc2d6e0d46d4b6b304f6/maintainability)](https://codeclimate.com/github/realpha/eitherway/maintainability) 4 | ![ci](https://github.com/realpha/eitherway/actions/workflows/ci.yml/badge.svg) 5 | [![coverage](https://api.codeclimate.com/v1/badges/dc2d6e0d46d4b6b304f6/test_coverage)](https://codeclimate.com/github/realpha/eitherway/test_coverage) 6 | [![deno](https://shield.deno.dev/x/eitherway)](https://deno.land/x/eitherway) 7 | [![npm](https://img.shields.io/npm/v/eitherway)](https://www.npmjs.com/package/eitherway) 8 | ![release](https://github.com/realpha/eitherway/actions/workflows/release.yml/badge.svg) 9 | 10 | > Yet Another Option and Result Implementation (**YAORI**) 11 | 12 | Safe abstractions for fallible flows inspired by F# and Rust. 13 | 14 | ## Disclaimer 15 | 16 | 🚧 This project is still under development, expect some breaking changes 🚧 17 | 18 | Please see this [tracking issue](https://github.com/realpha/eitherway/issues/9) 19 | for more information regarding when to expect a stable release. 20 | 21 | ## Contents 22 | 23 | - [Motivation](#motivation) 24 | - [Design Goals](#design-goals) 25 | - [`eitherway` in Action](#eitherway-in-action) 26 | - [Installation](#installation) 27 | - [API](#api) 28 | - [Overview](#overview) 29 | - [Design Decisions](#design-decisions) 30 | - [Additions](#additions) 31 | - [Option](#option) 32 | - [Result](#result) 33 | - [Task](#task) 34 | - [Best Practices](#best-practices) 35 | - [FAQ](#faq) 36 | - [Prior Art](#prior-art) 37 | - [License and Contributing](#license-and-contributing) 38 | 39 | ## Motivation 40 | 41 | Let's be honest: The community already nurtured a bunch of great projects, 42 | providing similar abstractions. The goal has always been to make it easier and 43 | more explicit to handle error cases under the 44 | ["errors are values" premise](https://www.youtube.com/watch?v=PAAkCSZUG1c&t=16m13s). 45 | After having worked with quite a few of these existing abstractions, a couple of 46 | issues arose/persisted: 47 | 48 | - **Overly strict**: Some of the existing implementations don't account for the 49 | variance emerging naturally in a structural type system. 50 | - **Onboarding is hard**: Most of the existing projects provide either very 51 | little or scattered documentation. Forcing new users to constantly switch 52 | context in order to understand how they can achieve their goal or if their 53 | chosen operation is suitable for their use case. 54 | - **Lack of async support**: Very few existing projects offer abstractions for 55 | working in an `async` context, none of them really being first class citizens. 56 | 57 | The goal here really is to make the abstractions provided by `eitherway` the 58 | most safe, productive and overall enjoyable to work with. Irrespective of 59 | experience or employed context. 60 | 61 | ## Design Goals 62 | 63 | `eitherway` is trying to close the gap between type-safety, idiom and 64 | productivity by focusing on the following goals: 65 | 66 | - **Pragmatism**: There is no need to strictly port something from another 67 | language with very different semantics. When decisions arrive, which call for 68 | a trade-off, `eitherway` will always try to offer a solution geared towards 69 | the constraints and idiom of Typescript. 70 | - **Compatibility**: Interacting with one of the structures defined by 71 | `eitherway` should be painless in the sense that things do what you would 72 | expect them to do in Typescript and are compatible with inherent language 73 | protocols (e.g. Iterator protocol). 74 | - **Safety**: Once an abstraction is instantiated, no inherent operation should 75 | ever panic. 76 | - **Performance**: All abstractions provided here should strive to amortize the 77 | cost of usage and be upfront about these costs. 78 | - **Documentation**: All structures come with full, inline documentation. Making 79 | it easy to understand what is currently happening and if the chosen operation 80 | is suitable for the desired use case. (Still very much in progress) 81 | 82 | ## `eitherway` in Action 83 | 84 | ```typescript 85 | import { Option, Result, Task } from "https://deno.land/x/eitherway/mod.ts"; 86 | 87 | const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); 88 | 89 | /** 90 | * A little API over-use to show what's possible. 91 | * This is not the most efficient way to write code, but still performs well 92 | * enough in benchmarks. 93 | */ 94 | 95 | function toUpperCase(input: string | undefined): Task { 96 | return Option(input) // All nullish values are None 97 | .okOrElse(() => TypeError("Input is undefined")) // Convert to Result 98 | .into((res) => Task.of(res)) // Push the result into an async context 99 | .map(async (str) => { 100 | await sleep(1); 101 | return str.toUpperCase(); 102 | }); 103 | } 104 | 105 | function stringToLength(input: string): Task { 106 | return Option.fromCoercible(input) // All falsy types are None 107 | .okOrElse(() => TypeError("Input string is empty")) 108 | .into((res) => Task.of(res)) 109 | .map(async (str) => { 110 | await sleep(1); 111 | return str.length; 112 | }); 113 | } 114 | 115 | function powerOfSelf(input: number): Task { 116 | return Option.fromCoercible(input) 117 | .okOrElse(() => 118 | TypeError("Cannot perform computation with NaN, Infinity or 0") 119 | ) 120 | .into((res) => Task.of(res)) 121 | .andThen(async (n) => { // Promise> and Task can be used interchangeably for async composition 122 | await sleep(1); 123 | return Option.fromCoercible(Math.pow(n, n)) 124 | .okOrElse(() => TypeError("Cannot calculate result")); 125 | }); 126 | } 127 | 128 | function processString(input: string | undefined): Task { 129 | return toUpperCase(input) // Synchronous and asynchronous composition work the same 130 | .andThen(stringToLength) 131 | .andThen(powerOfSelf); 132 | } 133 | 134 | async function main(): Promise> { 135 | const result = await processString("foo"); // Task is of course awaitable 136 | 137 | return Task.of(result); // You can return Task as Promise> 138 | } 139 | 140 | main() 141 | .then((result) => { 142 | result // Result 143 | .inspect(console.log) 144 | .inspectErr(console.error); 145 | }) 146 | .catch((e) => "Unreachable!") 147 | .finally(() => console.log("DONE!")); 148 | ``` 149 | 150 | ### Installation 151 | 152 | #### Minimum Required Runtime Versions 153 | 154 | `eitherway` internally uses the 155 | [`Error.cause`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error/cause#browser_compatibility) 156 | configuration option and 157 | [`structuredClone`](https://developer.mozilla.org/en-US/docs/Web/API/structuredClone), 158 | therefore please make sure these versions are met: 159 | 160 | - `deno`: >=1.14. 161 | - `node`: >=17.0.0 162 | - `Browser`: [`Error.cause`](https://caniuse.com/?search=Error.cause) & 163 | [`structuredClone`](https://caniuse.com/?search=structuredClone) 164 | 165 | | Deno
deno | Node.js
node | IE / Edge
Edge | Chrome
Chrome | Firefox
Firefox | Safari
Safari | 166 | | --------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- | 167 | | `>=1.14.0` | `>=17.0.0` | `>=119` | `>=119` | `>=119` | `>=17.1` | 168 | 169 | #### `deno` 170 | 171 | ```typescript 172 | import { 173 | Err, 174 | None, 175 | Ok, 176 | Option, 177 | Result, 178 | Some, 179 | Task, 180 | } from "https://deno.land/x/eitherway/mod.ts"; 181 | ``` 182 | 183 | #### `node` 184 | 185 | ```bash 186 | (npm | pnpm | yarn) add eitherway 187 | ``` 188 | 189 | `esm`: 190 | 191 | ```typescript 192 | import { Err, None, Ok, Option, Result, Some, Task } from "npm:eitherway"; 193 | ``` 194 | 195 | `cjs`: 196 | 197 | ```javascript 198 | const { Err, Ok, Option, None, Result, Some, Task } = require("eitherway"); 199 | ``` 200 | 201 | ## API 202 | 203 | ### Overview 204 | 205 | On a high level, `eitherway` provides 3 basic abstractions, which have different 206 | use cases: 207 | 208 | - [`Option`](#option): Composable equivalent of the union `T | undefined`. 209 | Use this to handle the case of non-existence gracefully or assert a certain 210 | fact about a value. 211 | - [`Result`](#result): Composable equivalent of the union `T | E`. Use 212 | this to gracefully handle an happy-path and error-path without the need to 213 | throw exceptions. 214 | - [`Task`](#task): Composable equivalent of `Promise`. Same as 215 | `Result` but for asynchronous operations. 216 | 217 | #### Design Decisions 218 | 219 | If you are coming from other languages, or other libraries, you will be familiar 220 | with most parts already. A couple of things are handled differently though: 221 | 222 | - **Thinking in unions**: Union types are ubiquitous and a powerful feature of 223 | Typescript. The abstractions provided by `eitherway` were modeled to provide a 224 | tag to members of unions commonly used. As a consequence, there are no `safe` 225 | or `unchecked` variants for methods like `.unwrap()`. 226 | 227 | ```typescript 228 | import { Ok, Option, Result } from "https://deno.land/x/eitherway/mod.ts"; 229 | 230 | const opt: Option = Option("foo"); 231 | const res: Result = Ok(1); 232 | 233 | // Without narrowing, the union type is returned 234 | const maybeString: string | undefined = opt.unwrap(); 235 | const numOrError: number | TypeError = res.unwrap(); 236 | 237 | // The type can easily be narrowed though 238 | if (res.isErr()) { 239 | console.error(res.unwrap()); 240 | } 241 | 242 | const num: number = res.unwrap(); 243 | ``` 244 | 245 | - **Upholding basic invariants**: You CANNOT construct an instance of 246 | `Option` and you MUST NOT throw exceptions when returning 247 | `Result` or `Task` from a function. 248 | - **Don't panic**: Following the previous statements, `eitherway` does not throw 249 | or re-throw exceptions under normal operations. In fact, there are only 3 250 | scenarios, which lead to a panic at runtime: 251 | 1. Trying to shove a nullish value into `Some`. The compiler will not allow 252 | this, but if you perform a couple of type casts, or a library you depend on 253 | provides wrong type declarations, the `Some` constructor will throw an 254 | exception, when you end up trying to instantiate it with a nullish value. 255 | 2. Trying to lift a `Promise` or a function, which you've explicitly provided 256 | as infallible, into a `Result` or `Task` context and it ends up panicking. 257 | 3. You, despite being told multiple times not to do so, chose to panic in a 258 | function you've implicitly marked as infallible by returning a 259 | `Result`, a `Promise>` or a `Task`. 260 | - **Closure of operations**: All mapping and chaining operations are closed, 261 | meaning that they return an instance of the same abstraction as the one they 262 | were called on. 263 | 264 | #### Additions 265 | 266 | Some notable additions, which you may have been missing in other libraries: 267 | 268 | - **Composable side-effects**: `.tap()`, `.inspect()` and `.inspectErr()` 269 | methods. 270 | - **Pass-through conditionals**: `.andEnsure()` & `.orEnsure()` methods. 271 | - **Sync & Async feature parity**: `Result` and `Task` provide the 272 | same API for composing operations. Only the predicates `.isOk()` and 273 | `.isErr()` are not implemented on `Task` (for obvious reasons). 274 | - **Composability helpers**: Higher order `.lift()` and `.liftFallible()` 275 | functions. Eliminating the need to manually wrap library or existing code in 276 | many situations. 277 | - **Collection helpers**: Exposed via the namespaces `Options`, `Results` and 278 | `Tasks`, every abstraction provides functions to collect tuples, arrays and 279 | iterables into the base abstraction. 280 | 281 | ### Option 282 | 283 | 1. [Overview and factories](https://deno.land/x/eitherway/mod.ts?s=Option) 284 | 2. [Base interface](https://deno.land/x/eitherway/mod.ts?s=IOption) implemented 285 | by `Some` and `None` 286 | 3. [Collection helpers](https://deno.land/x/eitherway/mod.ts?s=Options) 287 | 288 | ### Result 289 | 290 | 1. [Overview and factories](https://deno.land/x/eitherway/mod.ts?s=Result) 291 | 2. [Base interface](https://deno.land/x/eitherway/mod.ts?s=IResult) implemented 292 | by `Ok` and `Err` 293 | 3. [Collection helpers](https://deno.land/x/eitherway/mod.ts?s=Results) 294 | 295 | ### Task 296 | 297 | 1. [Overview and factories](https://deno.land/x/eitherway/mod.ts?s=Task) 298 | 2. [Collection helpers](https://deno.land/x/eitherway/mod.ts?s=Tasks) 299 | 300 | ## Best Practices 301 | 302 | 1. **Computations - not data**: The abstractions provided by `eitherway` are 303 | meant to represent the results of computations, not data. 304 | 2. **Embrace immutability**: Don't mutate your state. That's it. 305 | 3. **Return early occasionally**: When building up longer pipelines of 306 | operations, especially if they involve synchronous and asynchronous 307 | operations, you may want to break out of a pipeline to not enqueue 308 | micro-tasks needlessly. The need to do this, does arise less frequently than 309 | one might think though. 310 | 4. **Unwrap at the edges**: Most application frameworks and library consumers 311 | expect that any errors are propagated through exceptions (and hopefully 312 | documented). Therefore, it's advised to unwrap `Option`s, `Result`s and 313 | `Task`s at the outer most layer of your code. In a simple CRUD application, 314 | this might be an error handling interceptor, a controller, or the 315 | implementation of your public API in case of a library. 316 | 5. **Some errors are unrecoverable**: In certain situations the "errors are 317 | values" premise falls short on practicality. 318 | [burntsushi](https://blog.burntsushi.net/unwrap/), 319 | [Rob Pike](https://go.dev/doc/effective_go#errors) and many others have 320 | already written extensively about this. When encountering a truly 321 | unrecoverable error or an impossible state, throwing a runtime exception is a 322 | perfectly valid solution. 323 | 6. **Discriminate but don't over-accumulate**: It's often very tempting, to just 324 | accumulate possible errors as a discriminated union when building out flows 325 | via composition of `Result` and `Task` pipelines and let the user or the next 326 | component in line figure out what to do next. This only works up to a certain 327 | point. Errors are important domain objects, and they should be modeled 328 | accordingly. 329 | 7. **Lift others up to help yourself out**: Use the 330 | [composability helpers](https://deno.land/x/eitherway/mod.ts?s=Result.liftFallible). 331 | They really reduce noise and speed up integrating external code a lot. 332 | 333 | ```typescript 334 | import { Option, Result } from "https://deno.land/x/eitherway/mod.ts"; 335 | import * as semver from "https://deno.land/std@0.206.0/semver/mod.ts"; 336 | 337 | const noInputProvidedError = Error("No input provided"); 338 | const toParseError = (e: unknown) => 339 | TypeError("Could not parse version", { cause: e }); 340 | 341 | const tryParse = Result.liftFallible( 342 | semver.parse, 343 | toParseError, 344 | ); 345 | 346 | const version = Option.from(Deno.args[0]) 347 | .okOr(noInputProvidedError) 348 | .andThen(tryParse); 349 | ``` 350 | 351 | ## FAQ 352 | 353 |

354 | Q: Why should I even use something like this? 355 | A: It's nice. Really. 356 | 357 | Explicit error types and built-in happy/error path selectors lead to expressive 358 | code which is often even more pleasant to read. 359 | 360 |
361 | Compare these examples, taken from the benchmark suite: 362 | 363 | Synchronous: 364 | 365 | ```typescript 366 | /* Classic exception style */ 367 | 368 | declare function toUpperCase(input: string | undefined): string; 369 | declare function stringToLength(input: string): number; 370 | declare function powerOfSelf(input: number): number; 371 | 372 | function processString(input: string | undefined): number { 373 | try { 374 | const upperCased = toUpperCase(input); 375 | const length = stringToLength(upperCased); 376 | return powerOfSelf(length); 377 | } catch (error: unknown) { 378 | if (error instanceof TypeError) { 379 | console.error(error.message); 380 | throw error; 381 | } 382 | throw new TypeError("Unknown error", { cause: error }); 383 | } 384 | } 385 | ``` 386 | 387 | ```typescript 388 | /* Equivalent Result flow */ 389 | 390 | import { Result } from "https://deno.land/x/eitherway/mod.ts"; 391 | 392 | declare function toUpperCase( 393 | input: string | undefined, 394 | ): Result; 395 | declare function stringToLength(input: string): Result; 396 | declare function powerOfSelf(input: number): Result; 397 | 398 | function processString(input: string | undefined): Result { 399 | return toUpperCase(input) 400 | .andThen(stringToLength) 401 | .andThen(powerOfSelf) 402 | .inspectErr((e) => console.error(e.message)); 403 | } 404 | ``` 405 | 406 | Asynchronous: 407 | 408 | ```typescript 409 | /* Classic exception style */ 410 | 411 | declare function toUpperCase(input: string | undefined): Promise; 412 | declare function stringToLength(input: string): Promise; 413 | declare function powerOfSelf(input: number): Promise; 414 | 415 | async function processString(input: string | undefined): Promise { 416 | try { 417 | const upperCased = await toUpperCase(input); 418 | const length = await stringToLength(upperCased); 419 | return await powerOfSelf(length); 420 | } catch (error: unknown) { 421 | if (error instanceof TypeError) { 422 | console.error(error.message); 423 | throw error; 424 | } 425 | throw new TypeError("Unknown error", { cause: error }); 426 | } 427 | } 428 | ``` 429 | 430 | ```typescript 431 | /* Equivalent Task flow */ 432 | 433 | import { Result, Task } from "https://deno.land/x/eitherway/mod.ts"; 434 | 435 | declare function toUpperCase( 436 | input: string | undefined, 437 | ): Task; 438 | declare function stringToLength( 439 | input: string, 440 | ): Promise>; 441 | declare function powerOfSelf(input: number): Task; 442 | 443 | function processString(input: string | undefined): Task { 444 | return toUpperCase(input) 445 | .andThen(stringToLength) 446 | .andThen(powerOfSelf) 447 | .inspectErr((e) => console.error(e.message)); 448 | } 449 | ``` 450 | 451 |
452 | 453 | Apart from making error cases explicit, the abstractions provided here foster a 454 | code style, which naturally builds up complex computations via composition of 455 | small, focused functions/methods, where boundaries are defined by values. Thus 456 | leading to a highly maintainable and easily testable code base. 457 | 458 | Even better: These abstractions come with practically no overhead (see the next 459 | section). 460 | 461 | Here are a couple of videos, explaining the general benefits in more detail: 462 | 463 | - ["Railway-oriented programming" by Scott Wlaschin](https://vimeo.com/113707214) 464 | - ["Boundaries" by Gary Bernhardt](https://www.destroyallsoftware.com/talks/boundaries) 465 | 466 |
467 | 468 |
469 | Q: What is the performance impact of using this? 470 | A: Practically none. 471 | 472 | You can run the benchmark suite yourself with `$ deno bench`. 473 | 474 | The benchmark results suggest, that for nearly all practical considerations 475 | there is no or virtually no overhead of using the abstractions provided by 476 | `eitherway` vs. a classic exception propagation style. 477 | 478 | Although the result and task flows were slightly faster in the runs below, it's 479 | important not to fall into a micro-optimization trap. The conclusion should not 480 | necessarily be "use eitherway, it's faster", but rather "use eitherway, it's 481 | practically free". 482 | 483 | The overall performance thesis is that by returning errors instead of throwing, 484 | catching and re-throwing exceptions, the instantiation costs of the abstractions 485 | provided here are amortized over call-stack depth & it's size, as well as the 486 | optimizations the linear return path allows, sometimes even leading to small 487 | performance improvements. This sounds plausible, and the results are not 488 | refuting the null hypothesis here, but benchmarking is hard and for most use 489 | cases, the difference really won't matter. 490 | 491 |
492 | Synchronous exception propagation vs. result chaining 493 | 494 | ```markdown 495 | cpu: Intel(R) Core(TM) i9-9880H CPU @ 2.30GHz runtime: deno 1.33.2 496 | (x86_64-apple-darwin) 497 | 498 | ## file:///projects/eitherway/bench/sync_bench.ts benchmark time (avg) (min … max) p75 p99 p995 499 | 500 | SyncExceptions 29.15 µs/iter (20.54 µs … 472 µs) 31.34 µs 38.22 µs 49.28 µs 501 | SyncResultFlow 15.49 µs/iter (11.07 µs … 441.17 µs) 15.44 µs 31.69 µs 43.37 µs 502 | 503 | summary SyncResultFlow 1.88x faster than SyncExceptions 504 | ``` 505 | 506 |
507 | 508 |
509 | Asynchronous exception propagation vs. task chaining 510 | 511 | ```markdown 512 | cpu: Intel(R) Core(TM) i9-9880H CPU @ 2.30GHz runtime: deno 1.33.2 513 | (x86_64-apple-darwin) 514 | 515 | ## file:///projects/eitherway/bench/async_bench.ts benchmark time (avg) (min … max) p75 p99 p995 516 | 517 | AsyncExceptions 24.78 ms/iter (22.08 ms … 25.55 ms) 25.46 ms 25.55ms 25.55ms 518 | TaskInstanceFlow 23.88 ms/iter (21.28 ms … 25.8 ms) 24.57 ms 25.8ms 25.8ms 519 | TaskOperatorFlow 24.21 ms/iter (21.33 ms … 25.73 ms) 25.36 ms 25.73ms 25.73ms 520 | TaskEarlyReturnFlow 24.04 ms/iter (20.36 ms … 25.47 ms) 25.42 ms 25.47ms 25.47ms 521 | 522 | summary TaskInstanceFlow 1.01x faster than TaskEarlyReturnFlow 1.01x faster than 523 | TaskOperatorFlow 1.04x faster than AsyncExceptions 524 | ``` 525 | 526 |
527 | 528 |
529 | Micro benchmarks 530 | If you have a highly performance sensitive use case, you should be using 531 | a different language. 532 | On a more serious note, also small costs can add up and as a user, you should 533 | know how high the costs are. So here are a few micro benchmarks: 534 | 535 | ```markdown 536 | cpu: Intel(R) Core(TM) i9-9880H CPU @ 2.30GHz runtime: deno 1.33.2 537 | (x86_64-apple-darwin) 538 | 539 | ## file:///projects/eitherway/bench/micro_bench.ts benchmark time (avg) (min … max) p75 p99 p995 540 | 541 | Promise.resolve(Ok) 44.33 ns/iter (35.81 ns … 106.41 ns) 44.6 ns 62.58 ns 542 | 72.56ns Task.succeed 105.43 ns/iter (88.44 ns … 227.26 ns) 108.97 ns 204.75 ns 543 | 212.54ns Promise.resolve(Err) 3.11 µs/iter (3.06 µs … 3.27 µs) 3.13 µs 3.27 µs 544 | 3.27 µs Task.fail 2.94 µs/iter (2.71 µs … 3.35 µs) 3.25 µs 3.35 µs 3.35 µs 545 | 546 | summary Promise.resolve(Ok) 2.38x faster than Task.succeed 66.41x faster than 547 | Task.fail 70.14x faster than Promise.resolve(Err) 548 | 549 | ## file:///projects/eitherway/bench/micro_bench.ts benchmark time (avg) (min … max) p75 p99 p995 550 | 551 | Ok 5.1 ns/iter (4.91 ns … 22.27 ns) 5.02 ns 8.62 ns 11.67 ns Err 4.88 ns/iter 552 | (4.7 ns … 17.93 ns) 4.81 ns 8.18 ns 10.52 ns Option 90.39 ns/iter (83.63 ns … 553 | 172.61 ns) 93.31 ns 135.19 ns 146.79 ns 554 | 555 | summary Err 1.05x faster than Ok 18.52x faster than Option 556 | 557 | ## file:///projects/eitherway/bench/micro_bench.ts benchmark time (avg) (min … max) p75 p99 p995 558 | 559 | Async Exception Propagation 9.08 µs/iter (8.95 µs … 9.26 µs) 9.18 µs 9.26 µs 560 | 9.26 µs Async Error Propagation 6.32 µs/iter (6.24 µs … 6.52 µs) 6.37 µs 6.52 µs 561 | 6.52 µs 562 | 563 | summary Async Error Propagation 1.44x faster than Async Exception Propagation 564 | ``` 565 | 566 |
567 |
568 | 569 |
570 | Q: Why can't I use Task as the return type of an async function? 571 | A: That's a general restriction of JavaScript. 572 | 573 | A function defined with the `async` keyword, must return a "system" `Promise`. 574 | Although `Task` (currently) is a proper subclass of `Promise`, it cannot 575 | be used in the Return Type Position of an async function, because it's _NOT_ a 576 | "system" promise (for lack of a better word). 577 | 578 | Since `Task` is a subclass of `Promise>`, it's possible to 579 | return it as such from an async function though or just await it. 580 | 581 | ```typescript 582 | import { Result, Task } from "https://deno.land/x/eitherway/mod.ts"; 583 | 584 | async function toTask(str: string): Promise> { 585 | return Task.succeed(str); 586 | } 587 | ``` 588 | 589 | Furthermore, `Task` is merely a composability extension for 590 | `Promise>`. As such, you can cheaply convert every 591 | `Promise` via the `Task.of()` constructor, or use the promise 592 | operators to compose your pipeline. 593 | 594 |
595 | 596 |
597 | Q: Why subclassing Promises instead of just providing a PromiseLike abstraction? 598 | A: For compatibility reasons. 599 | 600 | The drawback of the current implementation is that we cannot evaluate 601 | `Task` lazily. On the other hand, a lot of framework or library code is 602 | still (probably needlessly) invariant over `PromiseLike` types. Therefore 603 | subclassing the native `Promise` and allowing the users to treat 604 | `Promise>` and `Task` interchangeably in most situations, was 605 | the preferred solution. 606 | 607 |
608 | 609 | ## Prior Art 610 | 611 | - [neverthrow](https://github.com/supermacro/neverthrow) 612 | - [ts-result](https://github.com/vultix/ts-results) 613 | - [oxide.ts](https://github.com/traverse1984/oxide.ts) 614 | - [eventual-result](https://github.com/alexlafroscia/eventual-result) 615 | 616 | ## License and Contributing 617 | 618 | ### Contributing 619 | 620 |

Please see CONTRIBUTING for more information.

621 | 622 | ### License 623 | 624 |

Licensed under MIT license.

625 | --------------------------------------------------------------------------------