Repository: marpple/FxTS
Pull request #309: feat: add `when`
Author: login: freevue
Created: 2025-03-09T04:40:43Z | Merged: 2025-03-30T14:28:16Z

Fixes https://github.com/marpple/FxTS/issues/305

The initial when function was designed to always terminate the pipeline based on the given condition. However, after implementing it, I realized that it was functionally identical to throwIf.

To enhance its differentiation, I added the following features:

1. The first argument accepts a Boolean value.
2. If the function in the second argument returns a value, that value is passed to the next pipe. If it returns nothing, an error is thrown to terminate the pipeline.

If the existing throwIf function is deemed sufficient, the when function may not be necessary.


Diff:
diff --git a/src/index.ts b/src/index.ts
index 829a73971..7921b402b 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -64,6 +64,7 @@ import throwIf from "./throwIf";
 import toArray from "./toArray";
 import unicodeToArray from "./unicodeToArray";
 import unless from "./unless";
+import when from "./when";
 
 export {
   add,
@@ -136,6 +137,7 @@ export {
   throwIf,
   toArray,
   unicodeToArray,
+  when,
 };
 
 export * from "./Lazy/index";
diff --git a/src/when.ts b/src/when.ts
new file mode 100644
index 000000000..fffdff275
--- /dev/null
+++ b/src/when.ts
@@ -0,0 +1,75 @@
+import isUndefined from "./isUndefined";
+
+/**
+ * It returns the original value based on the condition of the first argument or the result of executing the function passed as the second argument.
+ *
+ * @example
+ * ```typescript
+ * when(
+ *   isNumber,
+ *   () => `This is number`
+ *   100,
+ * ); // This is number
+ * ```
+ *
+ * with pipe
+ *
+ * ```typescript
+ * pipe(
+ *   100,
+ *   when(
+ *     isNumber,
+ *     () => `This is number`
+ *   )
+ * ) // This is number
+ *
+ * pipe(
+ *   100,
+ *   when(
+ *     isNumber,
+ *     (value) => value * 2
+ *   )
+ * ) // 200
+ *
+ * pipe(
+ *   100,
+ *   when(
+ *     isString,
+ *     () => `This is number` // not work
+ *   ),
+ * ) // 100
+ * ```
+ */
+
+function when<T, S extends T, R>(
+  predicate: (input: T) => input is S,
+  callback: (input: S) => R,
+): (iterator: T) => Exclude<T, S>;
+function when<T, R>(
+  predicate: (input: T) => boolean,
+  callback: (input: T) => R,
+): (iterator: T) => T | R;
+function when<T, S extends T, R>(
+  predicate: (input: T) => input is S,
+  callback: (input: S) => R,
+  iterator: T,
+): Exclude<T, S>;
+function when<T, R>(
+  predicate: (input: T) => boolean,
+  callback: (input: T) => R,
+  iterator: T,
+): T | R;
+
+function when<T, R>(
+  predicate: (input: T) => boolean,
+  callback: (input: T) => R,
+  iterator?: T,
+) {
+  if (isUndefined(iterator))
+    return (currentIterator: T) => when(predicate, callback, currentIterator);
+  if (predicate(iterator)) return callback(iterator);
+
+  return iterator;
+}
+
+export default when;
diff --git a/test/when.spec.ts b/test/when.spec.ts
new file mode 100644
index 000000000..1f606a747
--- /dev/null
+++ b/test/when.spec.ts
@@ -0,0 +1,37 @@
+import { isString, pipe, when } from "../src";
+
+describe("when", function () {
+  const test = <T>(value: T) =>
+    when(
+      (value) => value === 100,
+      (value) => {
+        expect(value).toBeTruthy();
+      },
+      value,
+    );
+  const withPipe = <T>(value: T) =>
+    pipe(
+      value,
+      test,
+      when(isString, () => "Hello fxts"),
+    );
+
+  it("If the input value is a number, it will be filtered out by the first 'when' function. And throw undefined.", () => {
+    const INPUT_VALUE = 100;
+
+    test(INPUT_VALUE);
+    withPipe(INPUT_VALUE);
+  });
+  it("If the input value is a string, it will be filtered out by the second 'when' function. And return value is 'Hello fxts'", () => {
+    const INPUT_VALUE = "Hello World";
+
+    expect(test(INPUT_VALUE)).toBe("Hello World");
+    expect(withPipe(INPUT_VALUE)).toBe("Hello fxts");
+  });
+  it("If nothing matches, all 'when' functions will be passed.", () => {
+    const INPUT_VALUE = [1, 2, 3, 4];
+
+    expect(test(INPUT_VALUE)).toEqual(INPUT_VALUE);
+    expect(withPipe(INPUT_VALUE)).toEqual(INPUT_VALUE);
+  });
+});
diff --git a/type-check/when.test.ts b/type-check/when.test.ts
new file mode 100644
index 000000000..a96d61ce1
--- /dev/null
+++ b/type-check/when.test.ts
@@ -0,0 +1,28 @@
+import { isNumber, isString, sum, when } from "../src";
+import * as Test from "../src/types/Test";
+
+const { checks, check } = Test;
+
+const VALUE_1 = 100;
+const VALUE_2 = [1, 2, 3, 4];
+
+const check1 = when(isNumber, (value) => `${value}`, VALUE_1);
+const check2 = when(isString, (value) => `${value}`, VALUE_1);
+const check3 = when(
+  (list) => sum(list) >= 10,
+  (value) => `${value}`,
+  VALUE_2,
+);
+const check4 = when(
+  (list) => sum(list) > 10,
+  (value) => `${value}`,
+  VALUE_2,
+);
+
+checks([
+  check<typeof VALUE_1, 100, Test.Pass>(),
+  check<typeof check1, string, Test.Pass>(),
+  check<typeof check2, number, Test.Pass>(),
+  check<typeof check3, typeof VALUE_2 | string, Test.Pass>(),
+  check<typeof check4, typeof VALUE_2 | string, Test.Pass>(),
+]);


Reviews:
user: login: ppeeou
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-27T11:00:31Z
user: login: freevue
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-30T04:02:33Z
user: login: ppeeou
body: lgtm👍
state: APPROVED
author_association: MEMBER
submitted_at: 2025-03-30T14:28:10Z

Review comments:
diff_hunk: @@ -0,0 +1,31 @@
+import { isNumber, isString, pipe, when } from "../src";
path: test/when.spec.ts
user: login: ppeeou
body: ```suggestion
import {  isString, pipe, when } from "../src";
```
created_at: 2025-03-27T10:57:48Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,64 @@
+import isUndefined from "./isUndefined";
+
+/**
+ * It returns the original value based on the condition of the first argument or the result of executing the function passed as the second argument.
+ *
+ * @example
+ * ```ts
+ * pipe(
+ *   100,
+ *   when(
+ *     isNumber,
+ *     () => `This is number`
+ *   )
+ * ) // This is number
path: src/when.ts
user: login: ppeeou
body: Could you please add some generic examples, not just `pipe`?
created_at: 2025-03-27T11:00:25Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,64 @@
+import isUndefined from "./isUndefined";
+
+/**
+ * It returns the original value based on the condition of the first argument or the result of executing the function passed as the second argument.
+ *
+ * @example
+ * ```ts
+ * pipe(
+ *   100,
+ *   when(
+ *     isNumber,
+ *     () => `This is number`
+ *   )
+ * ) // This is number
path: src/when.ts
user: login: freevue
body: @ppeeou 

I have added additional explanations to the document.

Additionally, I have included a test code matching the given example and added it to the existing pipe to verify that it produces the same result.
created_at: 2025-03-30T04:02:33Z
author_association: CONTRIBUTOR

PR comments:
user: login: ppeeou
created_at: 2025-03-13T13:34:22Z
body: @freevuehub  thank you for your contribution🎉

As you wrote in the issue, `when` seems to be useful just because it doesn't return an error if the predicate condition is met, unlike `throwIf`.
I think it would be good if you added it as you wrote in the issue!


I also request that you write a type test.
https://github.com/marpple/FxTS/tree/main/type-check
author_association: MEMBER
user: login: freevue
created_at: 2025-03-27T08:40:08Z
body: @ppeeou 
I have written it.
author_association: CONTRIBUTOR