Repository: marpple/FxTS (ts)

Issue:
Add `add` function
# Suggestion

<!--
  Please fill in each section completely. Thank you!
-->

## ⭐ Suggestion

<!-- What would you like to be added? -->

## 💻 Use Cases

```ts
add(1, 2); // 3
await add(1, Promise.resolve(3)); // 4

add('a', 'b'); // 'ab'
await add('a', Promise.resolve('c')); // 'ac'
```

Pull request description:
feat: add `add` function
Fixes #84 


Patch:
diff --git a/src/add.ts b/src/add.ts
new file mode 100644
index 00000000..e0e280cc
--- /dev/null
+++ b/src/add.ts
@@ -0,0 +1,81 @@
+import isNumber from "./isNumber";
+import isString from "./isString";
+import Awaited from "./types/Awaited";
+
+// prettier-ignore
+type ReturnAddType<T, A extends T, B extends T> = 
+  | [A] extends [Promise<Awaited<T>>]
+  ? Promise<Awaited<T>>
+  : [B] extends [Promise<Awaited<T>>]
+  ? Promise<Awaited<T>>
+  : Awaited<T>;
+
+function sync(a: number | string, b: number | string) {
+  if (isNumber(a) && isNumber(b)) {
+    return a + b;
+  }
+
+  if (isString(a) && isString(b)) {
+    return a + b;
+  }
+
+  throw new TypeError("'a' or 'b' must be type of number or string");
+}
+
+async function async(a: Promise<number | string>, b: Promise<number | string>) {
+  return sync(await a, await b);
+}
+
+/**
+ * Adds two values.
+ *
+ * @example
+ * ```ts
+ * add(1, 2); // 3
+ * await add(1, Promise.resolve(2)); // 3
+ * await add(Promise.resolve(1), 2); // 3
+ * await add(Promise.resolve(1), Promise.resolve(2)); // 3
+ *
+ * add('a', 'b'); // 'ab'
+ * await add('a', Promise.resolve('b')); // 'ab'
+ * await add(Promise.resolve('a'), 'b'); // 'ab'
+ * await add(Promise.resolve('a'), Promise.resolve('b')); // 'ab'
+ * ```
+ */
+function add<
+  A extends number | Promise<number>,
+  B extends number | Promise<number>,
+>(a: A, b: B): ReturnAddType<number | Promise<number>, A, B>;
+function add<
+  A extends string | Promise<string>,
+  B extends string | Promise<string>,
+>(a: A, b: B): ReturnAddType<string | Promise<string>, A, B>;
+
+function add<
+  A extends number | Promise<number>,
+  B extends number | Promise<number>,
+>(a: A): (b: B) => ReturnAddType<number | Promise<number>, A, B>;
+
+function add<
+  A extends string | Promise<string>,
+  B extends string | Promise<string>,
+>(a: A): (b: B) => ReturnAddType<string | Promise<string>, A, B>;
+
+function add<
+  A extends string | number | Promise<string | number>,
+  B extends string | number | Promise<string | number>,
+>(a: A, b?: B): number | string | Promise<string | number> | ((b: B) => any) {
+  if (b === undefined) {
+    return (b: B) => {
+      return add(a as any, b as any);
+    };
+  }
+
+  if (a instanceof Promise || b instanceof Promise) {
+    return async(Promise.resolve(a), Promise.resolve(b));
+  }
+
+  return sync(a, b);
+}
+
+export default add;
diff --git a/src/index.ts b/src/index.ts
index 222cba20..ae41e4e4 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -1,3 +1,4 @@
+import add from "./add";
 import apply from "./apply";
 import consume from "./consume";
 import countBy from "./countBy";
@@ -15,6 +16,7 @@ import indexBy from "./indexBy";
 import isArray from "./isArray";
 import isEmpty from "./isEmpty";
 import isNil from "./isNil";
+import isNumber from "./isNumber";
 import isObject from "./isObject";
 import isString from "./isString";
 import join from "./join";
@@ -39,6 +41,7 @@ import tap from "./tap";
 import toArray from "./toArray";
 
 export {
+  add,
   apply,
   consume,
   countBy,
@@ -58,6 +61,7 @@ export {
   isArray,
   isEmpty,
   isNil,
+  isNumber,
   isObject,
   isString,
   join,
diff --git a/src/isNumber.ts b/src/isNumber.ts
new file mode 100644
index 00000000..a81e1019
--- /dev/null
+++ b/src/isNumber.ts
@@ -0,0 +1,14 @@
+/**
+ * Returns true if `n` is a Number.
+ *
+ * @example
+ * ```ts
+ * isNumber("a"); // true
+ * isNumber(2); // false
+ * ```
+ */
+function isNumber(n: unknown): n is number {
+  return typeof n === "number";
+}
+
+export default isNumber;


Test patch:
diff --git a/test/add.spec.ts b/test/add.spec.ts
new file mode 100644
index 00000000..ce3441fe
--- /dev/null
+++ b/test/add.spec.ts
@@ -0,0 +1,39 @@
+import { add, pipe } from "../src";
+
+describe("add", function () {
+  describe("sync", function () {
+    it.each([
+      [1, 2, 3],
+      ["a", "b", "ab"],
+    ])("should add two values %s %s", function (a, b, result) {
+      expect(add(a as any, b as any)).toEqual(result);
+    });
+
+    it("should be able to be used as a curried function in the pipeline", function () {
+      const res1 = pipe(1, add(2));
+      expect(res1).toEqual(3);
+      const res2 = pipe("b", add("a"));
+      expect(res2).toEqual("ab");
+    });
+  });
+
+  describe("async", function () {
+    it.each([
+      [Promise.resolve(1), 2, 3],
+      [1, Promise.resolve(2), 3],
+      [Promise.resolve(1), Promise.resolve(2), 3],
+      [Promise.resolve("a"), "b", "ab"],
+      ["a", Promise.resolve("b"), "ab"],
+      [Promise.resolve("a"), Promise.resolve("b"), "ab"],
+    ])("should add two values %s %s", async function (a, b, result) {
+      expect(await add(a as Promise<any>, b as Promise<any>)).toEqual(result);
+    });
+
+    it("should be able to be used as a curried function in the pipeline", async function () {
+      const res1 = await pipe(1, add(Promise.resolve(2)));
+      expect(res1).toEqual(3);
+      const res2 = await pipe("b", add(Promise.resolve("a")));
+      expect(res2).toEqual("ab");
+    });
+  });
+});
diff --git a/test/isNumber.spec.ts b/test/isNumber.spec.ts
new file mode 100644
index 00000000..706d114f
--- /dev/null
+++ b/test/isNumber.spec.ts
@@ -0,0 +1,16 @@
+import { isNumber } from "../src/index";
+
+describe("isNumber", function () {
+  it.each([undefined, null, true, "1", Symbol("a"), () => null])(
+    "given non number then should return false",
+    function (s) {
+      const result = isNumber(s);
+      expect(result).toEqual(false);
+    },
+  );
+
+  it("given number then should return true", function () {
+    const result = isNumber(2);
+    expect(result).toEqual(true);
+  });
+});
diff --git a/type-check/add.test.ts b/type-check/add.test.ts
new file mode 100644
index 00000000..a931653d
--- /dev/null
+++ b/type-check/add.test.ts
@@ -0,0 +1,36 @@
+import * as Test from "../src/types/Test";
+import { add, pipe } from "../src";
+
+const { checks, check } = Test;
+
+const res1 = add(1, 2);
+const res2 = add("a", "b");
+const res3 = add(Promise.resolve(1), 2);
+const res4 = add(1, Promise.resolve(2));
+const res5 = add(Promise.resolve(1), Promise.resolve(2));
+const res6 = add(Promise.resolve("a"), "b");
+const res7 = add("a", Promise.resolve("b"));
+const res8 = add(Promise.resolve("a"), Promise.resolve("b"));
+const res9 = pipe(2, add(1));
+const res10 = pipe("b", add("a"));
+const res11 = pipe(Promise.resolve(2), add(1));
+const res12 = pipe(Promise.resolve("b"), add("a"));
+const res13 = add(2)(3);
+const res14 = add("a")("b");
+
+checks([
+  check<typeof res1, number, Test.Pass>(),
+  check<typeof res2, string, Test.Pass>(),
+  check<typeof res3, Promise<number>, Test.Pass>(),
+  check<typeof res4, Promise<number>, Test.Pass>(),
+  check<typeof res5, Promise<number>, Test.Pass>(),
+  check<typeof res6, Promise<string>, Test.Pass>(),
+  check<typeof res7, Promise<string>, Test.Pass>(),
+  check<typeof res8, Promise<string>, Test.Pass>(),
+  check<typeof res9, number, Test.Pass>(),
+  check<typeof res10, string, Test.Pass>(),
+  check<typeof res11, Promise<number>, Test.Pass>(),
+  check<typeof res12, Promise<string>, Test.Pass>(),
+  check<typeof res13, number, Test.Pass>(),
+  check<typeof res14, string, Test.Pass>(),
+]);
diff --git a/type-check/isNumber.test.ts b/type-check/isNumber.test.ts
new file mode 100644
index 00000000..751d82e8
--- /dev/null
+++ b/type-check/isNumber.test.ts
@@ -0,0 +1,14 @@
+import * as Test from "../src/types/Test";
+import { isNumber, pipe } from "../src";
+
+const { checks, check } = Test;
+
+const res1 = isNumber(1);
+const res2 = isNumber("a");
+const res3 = pipe(1, isNumber);
+
+checks([
+  check<typeof res1, boolean, Test.Pass>(),
+  check<typeof res2, boolean, Test.Pass>(),
+  check<typeof res3, boolean, Test.Pass>(),
+]);