From 2668456f8f0b01537a438444da618358271cd075 Mon Sep 17 00:00:00 2001 From: Tyler Holewinski Date: Tue, 7 Nov 2023 12:50:15 -0500 Subject: [PATCH] WrappedMaybe: `.try`/`.tryTake` (#81) --- .changeset/serious-moons-yell.md | 5 ++ src/maybe.test.ts | 34 +++++++++++ src/maybe.ts | 96 +++++++++++++++++--------------- 3 files changed, 91 insertions(+), 44 deletions(-) create mode 100644 .changeset/serious-moons-yell.md diff --git a/.changeset/serious-moons-yell.md b/.changeset/serious-moons-yell.md new file mode 100644 index 0000000..93679eb --- /dev/null +++ b/.changeset/serious-moons-yell.md @@ -0,0 +1,5 @@ +--- +"@bryx-inc/ts-utils": patch +--- + +Introduces `try` and `tryTake` to `WrappedMaybe` diff --git a/src/maybe.test.ts b/src/maybe.test.ts index b521a43..f702088 100644 --- a/src/maybe.test.ts +++ b/src/maybe.test.ts @@ -1,3 +1,4 @@ +import { pipe } from "./function"; import { expectMaybe, FormalMaybe, @@ -11,6 +12,7 @@ import { unwrapOrUndef, withSome, } from "./maybe"; +import { throwError } from "./errors"; const emptyMaybe: Maybe = null; const filledMaybe: Maybe = "foo"; @@ -242,6 +244,38 @@ describe("maybe()", () => { expect(maybe(thing2)?.take((it) => it.length)).toEqual(undefined); }); + it('should handle the "try" method correctly', () => { + const thing = "1234" as Maybe; + const thing2 = "foo" as Maybe; + + function parse(str: string) { + return pipe(Number.parseInt(str), (it) => (!Number.isNaN(it) ? it : throwError("Failed to parse"))); + } + + expect( + maybe(thing) + ?.try((it) => parse(it)) + ?.take() ?? null, + ).toEqual(1234); + expect( + maybe(thing2) + ?.try((it) => parse(it)) + ?.take() ?? null, + ).toEqual(null); + }); + + it('should handle the "tryTake" method correctly', () => { + const thing = "1234" as Maybe; + const thing2 = "foo" as Maybe; + + function parse(str: string) { + return pipe(Number.parseInt(str), (it) => (!Number.isNaN(it) ? it : throwError("Failed to parse"))); + } + + expect(maybe(thing)?.tryTake((it) => parse(it)) ?? null).toEqual(1234); + expect(maybe(thing2)?.tryTake((it) => parse(it)) ?? null).toEqual(null); + }); + it('should handle "also" method correctly', () => { const thing = "foo" as Maybe; const thing2 = null as Maybe; diff --git a/src/maybe.ts b/src/maybe.ts index 13952bd..aacab0c 100644 --- a/src/maybe.ts +++ b/src/maybe.ts @@ -1,3 +1,5 @@ +import { tryOr } from "./function"; + /** * A shorthand type for `T | null`. */ @@ -392,49 +394,55 @@ export class FormalMaybe { } } -type NullOrUndefined = null | undefined; - -type WrappedMaybe = T extends NullOrUndefined - ? WrappedMaybe> | Extract - : { - /** Calls the specified mapping with the wrapped value as its argument and returns the result */ - let: (fn: (it: T) => E) => WrappedMaybe; - /** Calls the specified function with the wrapped value as its argument and returns the wrapped value */ - also: (fn: (it: T) => void) => WrappedMaybe; - /** Returns the taken value if it satisfies the given predicate, otherwise returns null */ - takeIf: (predicate: (it: T) => boolean) => T | null; - /** Returns the taken value unless it satisfies the given predicate, in which case it returns null */ - takeUnless: (predicate: (it: T) => boolean) => T | null; - /** Returns the wrapped value unless it satisfies the given predicate, in which case it returns null */ - if: (predicate: (it: T) => boolean) => WrappedMaybe | null; - /** Returns the wrapped value unless it satisfies the given predicate, in which case it returns null */ - unless: (predicate: (it: T) => boolean) => WrappedMaybe | null; - /** Returns the wrapped value it was called with. A mapping may also be passed to mimic the behavor of `.let(fn).take()` */ - take: (fn?: (it: T) => E) => E; - } & NonNullable; +export function maybe(inner: T): T extends T ? (T extends null | undefined ? T : WrappedMaybe>) : never { + if (inner == null) return null as T extends T ? (T extends null | undefined ? T : WrappedMaybe>) : never; + else return new WrappedMaybe(inner) as T extends T ? (T extends null | undefined ? T : WrappedMaybe>) : never; +} -/** - * Creates a scoped value for chaining operations on a copy of the given wrapped value. - * - * @typeParam T The type of the wrapped value. - * @param wrapped The value to wrap - */ -export function maybe(wrapped: T): WrappedMaybe { - if (wrapped !== null && wrapped !== undefined) - return { - ...wrapped, - let: (fn: (it: T) => E) => maybe(fn(wrapped)), - takeIf: (predicate: (it: T) => boolean) => (predicate(wrapped) ? wrapped : null), - takeUnless: (predicate: (it: T) => boolean) => (predicate(wrapped) ? null : wrapped), - if: (predicate: (it: T) => boolean) => maybe(predicate(wrapped) ? wrapped : null), - unless: (predicate: (it: T) => boolean) => maybe(predicate(wrapped) ? null : wrapped), - take: (fn?: (it: T) => E) => (typeof fn == "function" ? fn(wrapped) : wrapped), - also: (fn: (it: T) => void) => { - fn(wrapped); - return maybe(wrapped); - }, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - } as any; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - else return wrapped as any; +export class WrappedMaybe { + constructor(private inner: T) {} + + let(mapping: (it: T) => E): WrappedMaybe { + return new WrappedMaybe(mapping(this.inner)); + } + + takeIf(predicate: (it: T) => boolean): T | null { + if (predicate(this.inner)) return this.inner; + else return null; + } + + takeUnless(predicate: (it: T) => boolean): T | null { + if (predicate(this.inner)) return null; + else return this.inner; + } + + if(predicate: (it: T) => boolean): WrappedMaybe | null { + if (predicate(this.inner)) return this; + else return null; + } + + unless(predicate: (it: T) => boolean): WrappedMaybe | null { + if (predicate(this.inner)) return null; + else return this; + } + + try(mapping: (it: T) => E): WrappedMaybe | null { + return tryOr(() => this.let(mapping), null); + } + + tryTake(mapping: (it: T) => E): E | null { + return this.try(mapping)?.take() ?? null; + } + + also(fn: (it: T) => unknown): WrappedMaybe { + fn(this.inner); + return this; + } + + take(): T; + take(mapping?: (it: T) => E): E; + + take(mapping?: (it: T) => E): T | E { + return typeof mapping == "function" ? mapping(this.inner) : this.inner; + } }