Repository: marpple/FxTS (ts)

Issue:
Add `juxt` function
# Suggestion

<!--
  Please fill in each section completely. Thank you!
-->

## ⭐ Suggestion

<!-- What would you like to be added? -->

Applies a list of function to a `Iterable/AsyncIterable` of values.

## 💻 Use Cases

<!-- What do you want to use this for? -->

```ts
juxt([Math.max, Math.min])(1,2,3,-4,5,6) // [6, -4]
```



Pull request description:
Add `juxt` function
Fixes #61


Patch:
diff --git a/src/index.ts b/src/index.ts
index 15e99394..6aebb9b6 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -20,6 +20,7 @@ import isNumber from "./isNumber";
 import isObject from "./isObject";
 import isString from "./isString";
 import join from "./join";
+import juxt from "./juxt";
 import last from "./last";
 import max from "./max";
 import min from "./min";
@@ -66,6 +67,7 @@ export {
   isObject,
   isString,
   join,
+  juxt,
   last,
   max,
   min,
diff --git a/src/juxt.ts b/src/juxt.ts
new file mode 100644
index 00000000..6326545c
--- /dev/null
+++ b/src/juxt.ts
@@ -0,0 +1,57 @@
+import type Append from "./types/Append";
+import type Arrow from "./types/Arrow";
+import type Cast from "./types/Cast";
+import type Tail from "./types/Tail";
+import type HasItem from "./types/HasItem";
+
+type ParameterTuples<FS extends Arrow[], T = []> = HasItem<FS> extends true
+  ? ParameterTuples<Tail<FS>, Append<Cast<T, any[]>, Parameters<FS[0]>>>
+  : T;
+
+type And<T extends any[], R = T[0]> = HasItem<T> extends true
+  ? And<Tail<T>, R & T[0]>
+  : R;
+
+type JuxtArgs<
+  FS extends Arrow[],
+  R = Cast<And<ParameterTuples<FS>>, any[]>,
+> = R extends never ? undefined : R;
+
+type ReturnTuples<FS extends Arrow[], T = []> = HasItem<FS> extends true
+  ? ReturnTuples<Tail<FS>, Append<Cast<T, any[]>, ReturnType<FS[0]>>>
+  : T;
+
+type JuxtReturnTypes<ARGS, FS extends Arrow[]> = ARGS extends undefined
+  ? void
+  : ReturnTuples<FS>;
+
+/**
+ * `juxt` applies a list of functions to a list of values.
+ *
+ * @example
+ * ```ts
+ * const range = juxt([Math.min, Math.max], [1, 2, 3, 4]); // [1, 4]
+ *
+ * // with pipe
+ * const entries = (obj: { a: number; b: number }) =>
+ *   pipe(
+ *     [Object.keys, Object.values] as const,
+ *     juxt,
+ *     (f) => f(obj),
+ *     apply(zip),
+ *     toArray,
+ *   );
+ *
+ * entries({ a: 1, b: 2 }); // [ ["a", 1], ["b", 2] ]
+ * ```
+ *
+ * see {@link https://fxts.dev/docs/pipe | pipe}, {@link https://fxts.dev/docs/apply | apply}
+ */
+
+function juxt<FS extends Arrow[]>(
+  fs: [...FS] | readonly [...FS],
+): (...args: JuxtArgs<FS>) => JuxtReturnTypes<JuxtArgs<FS>, FS> {
+  return (...args: JuxtArgs<FS>) => fs.map((f: Arrow) => f(...args)) as any;
+}
+
+export default juxt;
diff --git a/src/types/HasItem.ts b/src/types/HasItem.ts
new file mode 100644
index 00000000..9f2a1103
--- /dev/null
+++ b/src/types/HasItem.ts
@@ -0,0 +1,3 @@
+type HasItem<T extends any[]> = T extends [any, ...any] ? true : false;
+
+export default HasItem;


Test patch:
diff --git a/test/juxt.spec.ts b/test/juxt.spec.ts
new file mode 100644
index 00000000..6ada48d9
--- /dev/null
+++ b/test/juxt.spec.ts
@@ -0,0 +1,29 @@
+import { apply, juxt, pipe, toArray, zip } from "../src";
+
+describe("juxt", function () {
+  it("should return an empty array when the list of functions is absent", function () {
+    const res = juxt([])();
+    expect(res).toEqual([]);
+  });
+
+  it("should apply a list of functions to a list of values", function () {
+    const res = juxt([Math.min, Math.max])(1, 2, 3, -4, 5, 6, 7);
+    expect(res).toEqual([-4, 7]);
+  });
+
+  it("should be able to be used as a curried function in the pipeline", function () {
+    const entries = (obj: { a: number; b: number }) =>
+      pipe(
+        [Object.keys, Object.values] as const,
+        juxt,
+        (f) => f(obj),
+        apply(zip),
+        toArray,
+      );
+    const res = entries({ a: 1, b: 2 });
+    expect(res).toEqual([
+      ["a", 1],
+      ["b", 2],
+    ]);
+  });
+});
diff --git a/type-check/juxt.test.ts b/type-check/juxt.test.ts
new file mode 100644
index 00000000..8069ecd4
--- /dev/null
+++ b/type-check/juxt.test.ts
@@ -0,0 +1,34 @@
+import * as Test from "../src/types/Test";
+import { apply, pipe, juxt } from "../src";
+
+const { checks, check } = Test;
+
+declare function f1(a: number, b: string): string;
+declare function f2(a: number, b: string): boolean;
+// declare function f3(a: number, b: boolean): number;
+// declare function f4(a: number, b: string, c: boolean): number;
+
+const res1 = juxt([])();
+const res2 = juxt([f1, f2])(1, "2");
+
+// pipeline
+const res3 = pipe([1, 2, 3, 4], apply(juxt([Math.max, Math.min])));
+const res4 = pipe(juxt([f1, f2]), (f) => f(1, "2"));
+const res5 = pipe([f1, f2] as const, juxt, (f) => f(1, "2"));
+const res6 = pipe([f1, f2] as [typeof f1, typeof f2], juxt, (f) => f(1, "2"));
+
+/* error
+const res7 = pipe([f2, f3] as const, juxt, (f) => f());
+const res8 = pipe([f2, f3], juxt, (f) => f(1, "2"));
+const res9 = pipe([f2, f4], juxt, (f) => f(1, "2"));
+const res10 = pipe([f2, f4] as const, juxt, (f) => f(1, "2", true));
+*/
+
+checks([
+  check<typeof res1, [], Test.Pass>(),
+  check<typeof res2, [string, boolean], Test.Pass>(),
+  check<typeof res3, [number, number], Test.Pass>(),
+  check<typeof res4, [string, boolean], Test.Pass>(),
+  check<typeof res5, [string, boolean], Test.Pass>(),
+  check<typeof res6, [string, boolean], Test.Pass>(),
+]);