Repository: marpple/FxTS (ts)

Issue:
Add `evolve` function
# Suggestion

<!--
  Please fill in each section completely. Thank you!
-->

## ⭐ Suggestion

<!-- What would you like to be added? -->

## 💻 Use Cases

```ts
  const add1 = (a: number) => a + 1;
  const toUpper = (a: string) => a.toUpperCase();

  const obj = { a: 1, b: 'hello', c: true };

  evolve({ a: add1, b: toUpper, c: not }, obj);
  // { a: 2, b: 'HELLO', c: false }
```


Pull request description:
Add `evolve` function
Fixes #68 


Patch:
diff --git a/src/evolve.ts b/src/evolve.ts
new file mode 100644
index 00000000..b741aeb9
--- /dev/null
+++ b/src/evolve.ts
@@ -0,0 +1,57 @@
+import pipe from "./pipe";
+import identity from "./identity";
+
+type Transformation<O extends Record<string, any>> = {
+  [K in keyof O]?: (value: O[K]) => any;
+};
+
+type EvolveReturnType<
+  O extends Record<string, any>,
+  T extends Transformation<O>,
+> = {
+  [K in keyof O]: T[K] extends (value: O[K]) => any ? ReturnType<T[K]> : O[K];
+};
+
+/**
+ * Creates a new object by recursively evolving a shallow copy of object, according to the transformation functions.
+ *
+ * @example
+ * ```ts
+ * const add1String = (a: number) => String(a + 1);
+ * const obj = { a: 1, b: 2, c: { d: 3, e: 4 }, f: true };
+ * const transformation = {
+ *   a: add1String,
+ *   b: add1String,
+ *   c: (obj: { d: number; e: number }) =>
+ *     evolve({ d: add1String, e: add1String }, obj),
+ * };
+ * evolve(transformation, obj);
+ * // { a: "2", b: "3", c: { d: "4", e: "5" }, f: true }
+ * ```
+ */
+
+function evolve<O extends Record<string, any>, T extends Transformation<O>>(
+  transformation: T,
+): (obj: O) => EvolveReturnType<O, T>;
+
+function evolve<O extends Record<string, any>, T extends Transformation<O>>(
+  transformation: T,
+  obj: O,
+): EvolveReturnType<O, T>;
+
+function evolve<O extends Record<string, any>, T extends Transformation<O>>(
+  transformation: T,
+  obj?: O,
+): ((obj: O) => EvolveReturnType<O, T>) | EvolveReturnType<O, T> {
+  if (obj === undefined) {
+    return (obj: O) => evolve(transformation, obj);
+  }
+  return pipe(
+    Object.entries(obj),
+    (entries) =>
+      entries.map(([k, v]) => [k, (transformation[k] ?? identity)(v)]),
+    Object.fromEntries,
+  );
+}
+
+export default evolve;
diff --git a/src/index.ts b/src/index.ts
index 6aebb9b6..d33e4756 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -6,6 +6,7 @@ import curry from "./curry";
 import delay from "./delay";
 import each from "./each";
 import every from "./every";
+import evolve from "./evolve";
 import find from "./find";
 import findIndex from "./findIndex";
 import groupBy from "./groupBy";
@@ -51,6 +52,7 @@ export {
   delay,
   each,
   every,
+  evolve,
   find,
   findIndex,
   groupBy,
diff --git a/src/pickBy.ts b/src/pickBy.ts
index 98ea5057..c48a2085 100644
--- a/src/pickBy.ts
+++ b/src/pickBy.ts
@@ -12,7 +12,7 @@ import pipe from "./pipe";
 
 /**
  *
- * Returns a partial copy of an object containing only the keys that satisfy the supplied predicate.
+ * Returns a partial copy of an object which contains only the keys that satisfy the supplied predicate.
  *
  * @example
  * ```ts
diff --git a/src/reduce.ts b/src/reduce.ts
index f3dae1ba..bd80b296 100644
--- a/src/reduce.ts
+++ b/src/reduce.ts
@@ -24,7 +24,7 @@ async function async<A, B>(
 }
 
 /**
- * Also known as foldl, reduce boils down a list of values into a single value.
+ * Also known as foldl, this method boils down a list of values into a single value.
  *
  * @example
  * ```ts
diff --git a/src/sort.ts b/src/sort.ts
index 10adee79..a4fd5f4a 100644
--- a/src/sort.ts
+++ b/src/sort.ts
@@ -6,7 +6,7 @@ import pipe1 from "./pipe1";
 import toArray from "./toArray";
 
 /**
- * Returns an array which sorted according to the `f`
+ * Returns an array which is sorted by function `f`.
  *
  * @example
  * ```ts
diff --git a/src/toArray.ts b/src/toArray.ts
index a46a65c0..94aa6e6f 100644
--- a/src/toArray.ts
+++ b/src/toArray.ts
@@ -10,8 +10,8 @@ async function async<A>(iterable: AsyncIterable<A>): Promise<A[]> {
 }
 
 /**
- * Take item from Iterable/AsyncIterable and returns an array
- * It is recommended to use {@link https://fxts.dev/docs/pipe | pipe} together
+ * Takes item from Iterable/AsyncIterable and returns an array.
+ * It is recommended to use {@link https://fxts.dev/docs/pipe | pipe} together.
  *
  * @example
  * ```ts


Test patch:
diff --git a/test/evolve.spec.ts b/test/evolve.spec.ts
new file mode 100644
index 00000000..6b3c40e0
--- /dev/null
+++ b/test/evolve.spec.ts
@@ -0,0 +1,55 @@
+import { evolve, pipe } from "../src/index";
+
+describe("evolve", function () {
+  const add1 = (a: number) => a + 1;
+  const add1String = (a: number) => String(add1(a));
+
+  it("should return the transformed object", function () {
+    const obj = { a: 1, b: 2, c: 3 };
+    const transformation = {
+      a: add1String,
+      b: add1,
+      c: add1String,
+    };
+    const res = evolve(transformation, obj);
+    expect(res).toEqual({ a: "2", b: 3, c: "4" });
+  });
+
+  it("should apply the `identity` function to a property if there is no matched transform function", function () {
+    const obj = { a: 1, b: 2, c: 3 };
+    const transformation = {
+      b: add1,
+    };
+    const res = evolve(transformation, obj);
+    expect(res).toEqual({ a: 1, b: 3, c: 3 });
+  });
+
+  it("should be able to be used as a curried function in the pipeline", function () {
+    const obj = { a: 1, b: 2, c: 3 };
+    const transformation = {
+      a: add1String,
+      b: add1,
+      c: add1String,
+    };
+    const res = pipe(obj, evolve(transformation), Object.values);
+    expect(res).toEqual(["2", 3, "4"]);
+  });
+
+  it("should be handled nested objects", function () {
+    const add1String = (a: number) => String(a + 1);
+    const obj = { a: 1, b: 2, c: { d: 3, e: 4 }, f: true };
+    const transformation = {
+      a: add1String,
+      b: add1String,
+      c: (obj: { d: number; e: number }) =>
+        evolve({ d: add1String, e: add1String }, obj),
+    };
+    const res = evolve(transformation, obj);
+    expect(res).toEqual({
+      a: "2",
+      b: "3",
+      c: { d: "4", e: "5" },
+      f: true,
+    });
+  });
+});
diff --git a/type-check/evolve.test.ts b/type-check/evolve.test.ts
new file mode 100644
index 00000000..571bd23c
--- /dev/null
+++ b/type-check/evolve.test.ts
@@ -0,0 +1,55 @@
+import * as Test from "../src/types/Test";
+import { evolve, pipe } from "../src";
+
+const { checks, check } = Test;
+
+const obj = {
+  a: 0,
+  b: "1",
+  c: true,
+};
+
+const transform1 = {
+  a: Boolean,
+  b: Number,
+  c: String,
+};
+
+const transform2 = {
+  a: Boolean,
+  c: String,
+};
+
+const res1 = evolve(transform1, obj);
+const res2 = evolve(transform2, obj);
+
+// with pipe
+const res3 = pipe(obj, evolve(transform1));
+const res4 = pipe(obj, evolve(transform2));
+
+// nested object
+const obj2 = {
+  a: 0,
+  b: { c: 1, d: 2 },
+  e: 3,
+};
+
+const transform3 = {
+  a: (v: number) => v + 1,
+  b: (obj: typeof obj2["b"]) => evolve({ c: Boolean, d: () => null }, obj),
+  e: String,
+};
+
+const res5 = evolve(transform3, obj2);
+
+checks([
+  check<typeof res1, { a: boolean; b: number; c: string }, Test.Pass>(),
+  check<typeof res2, { a: boolean; b: string; c: string }, Test.Pass>(),
+  check<typeof res3, { a: boolean; b: number; c: string }, Test.Pass>(),
+  check<typeof res4, { a: boolean; b: string; c: string }, Test.Pass>(),
+  check<
+    typeof res5,
+    { a: number; b: { c: boolean; d: null }; e: string },
+    Test.Pass
+  >(),
+]);