Skip to content

Commit

Permalink
Introduce maybe(...) Wrapper (#70)
Browse files Browse the repository at this point in the history
  • Loading branch information
erwijet authored Oct 13, 2023
1 parent 16c8a4e commit 0bfe98f
Show file tree
Hide file tree
Showing 3 changed files with 113 additions and 1 deletion.
5 changes: 5 additions & 0 deletions .changeset/breezy-panthers-breathe.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@bryx-inc/ts-utils": minor
---

Introduces `maybe(...)` method to provide kotlin-style scope methods
68 changes: 67 additions & 1 deletion src/maybe.test.ts
Original file line number Diff line number Diff line change
@@ -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<string> = null;
const filledMaybe: Maybe<string> = "foo";
Expand Down Expand Up @@ -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<string>;
const thing2 = null as Maybe<string>;

expect(maybe(thing)?.value()).toEqual("foo");
expect(maybe(thing2)?.value).toEqual(undefined);
});

it('should handle "also" method correctly', () => {
const thing = "foo" as Maybe<string>;
const thing2 = null as Maybe<string>;

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);
});
});
41 changes: 41 additions & 0 deletions src/maybe.ts
Original file line number Diff line number Diff line change
Expand Up @@ -391,3 +391,44 @@ export class FormalMaybe<T> {
return this;
}
}

type NullOrUndefined = null | undefined;

type WrappedMaybe<T> = T extends NullOrUndefined
? WrappedMaybe<Exclude<T, NullOrUndefined>> | Extract<T, NullOrUndefined>
: {
/** Calls the specified function with the wrapped value as its argument and returns the result */
let: <E>(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<T> | 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<T>;
} & NonNullable<T>;

/**
* 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<T>(wrapped: T): WrappedMaybe<T> {
if (wrapped !== null && wrapped !== undefined)
return {
...wrapped,
let: <E>(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;
}

0 comments on commit 0bfe98f

Please sign in to comment.