Repository: true-myth/true-myth
Pull request #909 (2025-01-09 03:04:52)

Maybe: rename `wrapReturn` to `safe`
This leaves the names matching across `Maybe`, `Result`, and `Task`. The `Maybe` and `Result` versions compose very nicely with each others; you can do something like this to transform a function which returns `null` and may throw an error, for example:

```ts
import * as Maybe from 'true-myth/maybe';
import * as Result from 'true-myth/result';

function fnThatMayThrow(): string | null {
  let randomNumber = Math.random();
  if (randomNumber > 0.75) {
    throw new Error('lol, too high');
  } else if (randomNumber < 0.25) {
    return null;
  } else {
    return `you win! ${randomNumber}`;
  }
}

const fullySafe = Result.safe(Maybe.safe(fnThatMayThrow));
```

The `fullySafe` function created by composing the `safe` functions from the `result` and `maybe` modules returns `Result<Maybe<string>, Error>`.

Note that unfortunately the `Maybe` version of `safe` does *not* easily compose with `Task` because of the intervening `Promise`. We may add a helper to deal with that specific case in the future!

For now, keep but deprecate the existing name `wrapReturn`.


Patch:
diff --git a/src/maybe.ts b/src/maybe.ts
index 4f25488..61ce63c 100644
--- a/src/maybe.ts
+++ b/src/maybe.ts
@@ -1545,7 +1545,7 @@ export function get<T, K extends keyof T>(
   @param fn The function to transform; the resulting function will have the
             exact same signature except for its return type.
  */
-export function wrapReturn<
+export function safe<
   F extends AnyFunction,
   P extends Parameters<F>,
   R extends NonNullable<ReturnType<F>>,
@@ -1553,6 +1553,13 @@ export function wrapReturn<
   return (...params) => Maybe.of(fn(...params) as R);
 }
 
+/**
+  @alias for {@linkcode safe}.
+  @deprecated Switch to using {@linkcode safe} instead. This will be removed at
+    9.0.
+ */
+export const wrapReturn = safe;
+
 /**
   The public interface for the {@linkcode Maybe} class *as a value*: a
   constructor and the associated static properties.


Test patch:
diff --git a/test/interop.test.ts b/test/interop.test.ts
index 511453c..5de034f 100644
--- a/test/interop.test.ts
+++ b/test/interop.test.ts
@@ -1,6 +1,7 @@
-import { Maybe } from 'true-myth/maybe';
-import { Result } from 'true-myth/result';
-import { describe, expect, test } from 'vitest';
+import { safeToString } from 'true-myth/-private/utils';
+import { Maybe, safe as maybeSafe } from 'true-myth/maybe';
+import { Result, safe as resultSafe } from 'true-myth/result';
+import { describe, expect, expectTypeOf, test } from 'vitest';
 
 describe('nested Result and Maybe', () => {
   test('Result of Maybe', () => {
@@ -10,3 +11,41 @@ describe('nested Result and Maybe', () => {
     expect(inferenceFun()).toBeInstanceOf(Result);
   });
 });
+
+describe('composing safe', () => {
+  const ERR_MESSAGE = 'lol, too high';
+
+  function print(value: number): string {
+    return `you win! ${value}, yay`;
+  }
+
+  function fnThatMayThrow(input: number): string | null {
+    if (input > 0.75) {
+      throw new Error(ERR_MESSAGE);
+    } else if (input < 0.25) {
+      return null;
+    } else {
+      return print(input);
+    }
+  }
+
+  test('with a handler with Result', () => {
+    let safe = resultSafe(maybeSafe(fnThatMayThrow));
+    expectTypeOf(safe).toEqualTypeOf<(input: number) => Result<Maybe<string>, unknown>>();
+
+    expect(safe(1)).toEqual(Result.err(new Error(ERR_MESSAGE)));
+    expect(safe(0)).toEqual(Result.ok(Maybe.nothing()));
+    expect(safe(0.5)).toEqual(Result.ok(Maybe.just(print(0.5))));
+  });
+
+  test('without a handler with Result', () => {
+    const onError = (err: unknown) => safeToString(err);
+
+    let safe = resultSafe(maybeSafe(fnThatMayThrow), onError);
+    expectTypeOf(safe).toEqualTypeOf<(input: number) => Result<Maybe<string>, string>>();
+
+    expect(safe(1)).toEqual(Result.err(onError(new Error(ERR_MESSAGE))));
+    expect(safe(0)).toEqual(Result.ok(Maybe.nothing()));
+    expect(safe(0.5)).toEqual(Result.ok(Maybe.just(print(0.5))));
+  });
+});
diff --git a/test/maybe.test.ts b/test/maybe.test.ts
index 731a41b..01a71da 100644
--- a/test/maybe.test.ts
+++ b/test/maybe.test.ts
@@ -526,6 +526,31 @@ describe('`Maybe` pure functions', () => {
     expect(MaybeNS.get('wat', dict)).toEqual(MaybeNS.nothing());
   });
 
+  test('`safe`', () => {
+    const empty = '';
+    const emptyResult = MaybeNS.nothing();
+
+    const full = 'hello';
+    const fullResult = MaybeNS.just(full.length);
+
+    const mayBeNull = (s: string): number | null => (s.length > 0 ? s.length : null);
+    const mayNotBeNull = MaybeNS.safe(mayBeNull);
+
+    expect(mayNotBeNull(empty)).toEqual(emptyResult);
+    expect(mayNotBeNull(full)).toEqual(fullResult);
+
+    const mayBeUndefined = (s: string): number | undefined => (s.length > 0 ? s.length : undefined);
+    const mayNotBeUndefined = MaybeNS.safe(mayBeUndefined);
+
+    expect(mayNotBeUndefined(empty)).toEqual(emptyResult);
+    expect(mayNotBeUndefined(full)).toEqual(fullResult);
+
+    const returnsNullable = (): string | null => null;
+
+    const querySelector = MaybeNS.safe(returnsNullable);
+    expectTypeOf(querySelector).toEqualTypeOf<() => Maybe<string>>();
+  });
+
   test('`wrapReturn`', () => {
     const empty = '';
     const emptyResult = MaybeNS.nothing();