From 0bfe98fd463c3504af5ee567b59bfa75302cce07 Mon Sep 17 00:00:00 2001 From: Tyler Holewinski Date: Fri, 13 Oct 2023 16:52:33 -0400 Subject: [PATCH] Introduce `maybe(...)` Wrapper (#70) --- .changeset/breezy-panthers-breathe.md | 5 ++ src/maybe.test.ts | 68 ++++++++++++++++++++++++++- src/maybe.ts | 41 ++++++++++++++++ 3 files changed, 113 insertions(+), 1 deletion(-) create mode 100644 .changeset/breezy-panthers-breathe.md diff --git a/.changeset/breezy-panthers-breathe.md b/.changeset/breezy-panthers-breathe.md new file mode 100644 index 0000000..f8ace9b --- /dev/null +++ b/.changeset/breezy-panthers-breathe.md @@ -0,0 +1,5 @@ +--- +"@bryx-inc/ts-utils": minor +--- + +Introduces `maybe(...)` method to provide kotlin-style scope methods diff --git a/src/maybe.test.ts b/src/maybe.test.ts index b125c29..74445e5 100644 --- a/src/maybe.test.ts +++ b/src/maybe.test.ts @@ -1,4 +1,16 @@ -import { expectMaybe, FormalMaybe, intoMaybe, isNone, isSome, matchMaybe, Maybe, unwrapMaybe, unwrapOrUndef, withSome } from "./maybe"; +import { + expectMaybe, + FormalMaybe, + intoMaybe, + isNone, + isSome, + matchMaybe, + maybe, + Maybe, + unwrapMaybe, + unwrapOrUndef, + withSome, +} from "./maybe"; const emptyMaybe: Maybe = null; const filledMaybe: Maybe = "foo"; @@ -169,3 +181,57 @@ describe("take()", () => { expect(nullableValue.inner()).toBeNull(); }); }); + +describe("maybe()", () => { + it("should allow chaining methods and return the wrapped value", () => { + const wrappedValue = maybe(42); + const result = wrappedValue.let((it) => it * 2); + + expect(result).toBe(84); + }); + + it("should handle null or undefined values correctly", () => { + expect(maybe(null as string | null)?.let((it) => it.length)).toEqual(undefined); + }); + + it('should handle "takeUnless" method correctly', () => { + expect(maybe([1, 2, 3] as number[] | null)?.takeUnless((it) => it.length == 0)).toEqual([1, 2, 3]); + expect(maybe([] as number[] | null)?.takeUnless((it) => it.length == 0)).toEqual(null); + }); + + it('should handle "takeIf" method correctly', () => { + expect(maybe([1, 2, 3] as number[] | null)?.takeIf((it) => it.length > 0)).toEqual([1, 2, 3]); + expect(maybe([] as number[] | null)?.takeIf((it) => it.length > 0)).toEqual(null); + }); + + it("should properly eject the wrapped value", () => { + const thing = "foo" as Maybe; + const thing2 = null as Maybe; + + expect(maybe(thing)?.value()).toEqual("foo"); + expect(maybe(thing2)?.value).toEqual(undefined); + }); + + it('should handle "also" method correctly', () => { + const thing = "foo" as Maybe; + const thing2 = null as Maybe; + + const fn = jest.fn(); + + expect( + maybe(thing)?.also((it) => { + fn(it); + return "bar"; + }), + ).toEqual("foo"); + + expect( + maybe(thing2)?.also((it) => { + fn(it); + return "bar"; + }), + ).toEqual(undefined); + + expect(fn).toBeCalledTimes(1); + }); +}); diff --git a/src/maybe.ts b/src/maybe.ts index e6ed730..6131996 100644 --- a/src/maybe.ts +++ b/src/maybe.ts @@ -391,3 +391,44 @@ export class FormalMaybe { return this; } } + +type NullOrUndefined = null | undefined; + +type WrappedMaybe = T extends NullOrUndefined + ? WrappedMaybe> | Extract + : { + /** Calls the specified function with the wrapped value as its argument and returns the result */ + let: (fn: (it: T) => E) => E; + /** Calls the specified function with the wrapped value as its argument and returns the wrapped value */ + also: (fn: (it: T) => void) => T; + /** Returns the value if it satisfies the given predicate, otherwise returns null */ + takeIf: (predicate: (it: T) => boolean) => NonNullable | null; + /** Returns the value unless it satisfies the given predicate, in which case it returns null */ + takeUnless: (predicate: (it: T) => boolean) => T | null; + /** Returns the `this` value it was called with */ + value: () => NonNullable; + } & NonNullable; + +/** + * 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) => fn(wrapped), + takeIf: (predicate: (it: T) => boolean) => (predicate(wrapped) ? wrapped : null), + takeUnless: (predicate: (it: T) => boolean) => (predicate(wrapped) ? null : wrapped), + value: () => wrapped, + also: (fn: (it: T) => void) => { + fn(wrapped); + return 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; +}