Repository: remeda/remeda
Pull request #454 (2024-01-09 12:27:34)

feat: add `evolve` function 
close https://github.com/remeda/remeda/issues/452


---

Make sure that you:

- [x] Typedoc added for new methods and updated for changed
- [x] Tests added for new methods and updated for changed
- [x] New methods added to `src/index.ts`
- [x] New methods added to `mapping.md`

---

<details><summary>We use semantic PR titles to automate the release process!</summary>

https://conventionalcommits.org

PRs should be titled following using the format: `< TYPE >(< scope >)?: description`

### Available Types:

- `feat`: new functions, and changes to a function's type that would impact users.
- `fix`: changes to the runtime behavior of an existing function, or refinements to it's type that shouldn't impact most users.
- `perf`: changes to function implementations that improve a functions _runtime_ performance.
- `refactor`: changes to function implementations that are neither `fix` nor `perf`
- `test`: tests-only changes (transparent to users of the function).
- `docs`: changes to the documentation of a function **or the documentation site**.
- `build`, `ci`, `style`, `chore`, and `revert`: are only relevant for the internals of the library.

For scope put the name of the function you are working on (either new or
existing).

</details>


codesandbox-ci[bot]: This pull request is automatically built and testable in [CodeSandbox](https://codesandbox.io).

  To see build info of the built libraries, click [here](https://ci.codesandbox.io/status/remeda/remeda/pr/454/builds/458067) or the icon next to each commit SHA.

Latest deployment of this branch, based on commit a649888dcce93ff30313460a92fdaa9dc10a5b94:

|Sandbox| Source |
|--|--|
|[remeda-example-vanilla](https://codesandbox.io/s/remeda-example-vanilla-whfdpt)| Configuration |

codecov[bot]: ## [Codecov](https://app.codecov.io/gh/remeda/remeda/pull/454?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=remeda) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Comparison is base [(`6f876dd`)](https://app.codecov.io/gh/remeda/remeda/commit/6f876dd1777bded8995b53b19f6c799b9f86f2e4?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=remeda) 98.29% compared to head [(`a649888`)](https://app.codecov.io/gh/remeda/remeda/pull/454?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=remeda) 98.36%.
> Report is 7 commits behind head on master.


<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##           master     #454      +/-   ##
==========================================
+ Coverage   98.29%   98.36%   +0.07%     
==========================================
  Files         114      120       +6     
  Lines        7384     7724     +340     
  Branches      688      712      +24     
==========================================
+ Hits         7258     7598     +340     
  Misses        123      123              
  Partials        3        3              
```



</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/remeda/remeda/pull/454?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=remeda).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=remeda).

eranhirsch: Hey, just wanted to ping that I didn't get to reviewing this yet and I'm sorry for the delays. I will try to take a look at it now, but might not finish the review...
fneco: I'm happy to hear from you. Thank you for your concern. I will wait patiently.
fneco: Thank you for your kind review.

## ❶ about type

> In short, take the typing of the input, T, and construct the type for the transformations object from it, and not the other way around.

I change the implementation like this:
* In case of Data First, data arg type construct the transformations object type (I added new `Evolver` type)
* In case of Data Last, vice versa, transformations object type construct data arg type (in the same manner as before)

As a result, the noted problems were resolved as shown in the image below.

![image](https://github.com/remeda/remeda/assets/67921654/73b0788a-f31d-4469-ab35-6f3e32ae0814)

## ❷ about changing implementation

As I mentioned [in past comments](https://github.com/remeda/remeda/pull/454#discussion_r1446832361),
I found it difficult to meet Remeda's requirements for type safety while maintaining the lambda implementation.
So, I remove functionality handling functions tuple (e.g. `[add(1), add(-1) ]`).
Accordingly, the type and implementation were modified.


We apologize for the inconvenience, but we would appreciate a re-review.
fneco: Thanks for your thoughtful review. The code has significantly improved as a result. I've addressed the noted issues, but there's one point I'd like to discuss further. Please see [my comment here (3)](https://github.com/remeda/remeda/pull/454#discussion_r1490324767) regarding that.

Patch:
diff --git a/mapping.md b/mapping.md
index 5814432..8851e57 100644
--- a/mapping.md
+++ b/mapping.md
@@ -28,6 +28,7 @@ documentation when migrating._
 | [`dropLastWhile`](https://remedajs.com/docs/#dropLastWhile)         | [`dropRightWhile`](https://lodash.com/docs/4.17.15#dropRightWhile)       | [`dropLastWhile`](https://ramdajs.com/docs/#dropLastWhile)         |
 | [`dropWhile`](https://remedajs.com/docs/#dropWhile)                 | [`dropWhile`](https://lodash.com/docs/4.17.15#dropWhile)                 | [`dropWhile`](https://ramdajs.com/docs/#dropWhile)                 |
 | [`equals`](https://remedajs.com/docs/#equals)                       | [`isEqual`](https://lodash.com/docs/4.17.15#isEqual)                     | [`equals`](https://ramdajs.com/docs/#equals)                       |
+| [`evolve`](https://remedajs.com/docs/#evolve)                       |                                                                          | [`evolve`](https://ramdajs.com/docs/#evolve)                       |
 | [`filter`](https://remedajs.com/docs/#filter)                       | [`filter`](https://lodash.com/docs/4.17.15#filter)                       | [`filter`](https://ramdajs.com/docs/#filter)                       |
 | [`find`](https://remedajs.com/docs/#find)                           | [`find`](https://lodash.com/docs/4.17.15#find)                           | [`find`](https://ramdajs.com/docs/#find)                           |
 | [`findIndex`](https://remedajs.com/docs/#findIndex)                 | [`findIndex`](https://lodash.com/docs/4.17.15#findIndex)                 | [`findIndex`](https://ramdajs.com/docs/#findIndex)                 |
diff --git a/src/evolve.ts b/src/evolve.ts
new file mode 100644
index 0000000..8eff961
--- /dev/null
+++ b/src/evolve.ts
@@ -0,0 +1,152 @@
+import type { IterableContainer } from './_types';
+import { purry } from './purry';
+import { toPairs } from './toPairs';
+
+/**
+ * basic structure of `evolver` parameter of the function `evolve`.
+ */
+type GenericEvolver = {
+  readonly [P in string]: ((data: unknown) => unknown) | GenericEvolver;
+};
+
+/**
+ * Creates an assumed `evolver` type from the type of `data` argument.
+ * @example
+ * interface Data {
+ *   id: number;
+ *   quartile: Array<number>;
+ *   time?: { elapsed: number; remaining?: number };
+ * }
+ * type Nested = Evolver<Data>; //  => type Nested = {
+ * //   id?: ((data: number) => unknown) | undefined;
+ * //   quartile?: ((data: number[]) => unknown) | undefined;
+ * //   time?:
+ * //     | ((data: { elapsed: number; remaining?: number | undefined }) => unknown)
+ * //     | {
+ * //         elapsed?: ((data: number) => unknown) | undefined;
+ * //         remaining?: ((data: number) => unknown) | undefined;
+ * //       }
+ * //     | undefined;
+ * // };
+ */
+type Evolver<T> = T extends object
+  ? T extends IterableContainer
+    ? never
+    : {
+        readonly [K in keyof T]?:
+          | ((data: Required<T>[K]) => unknown)
+          | Evolver<T[K]>;
+      }
+  : never;
+
+/**
+ * Creates return type from the type of arguments of `evolve`.
+ */
+type Evolved<T, E> = T extends object
+  ? {
+      -readonly [K in keyof T]: K extends keyof E
+        ? E[K] extends (...arg: any) => infer R
+          ? R
+          : Evolved<T[K], E[K]>
+        : Required<T>[K];
+    }
+  : T;
+
+/**
+ * Creates a new object by applying functions that is included in `evolver` object parameter
+ * to the `data` object parameter according to their corresponding path.
+ *
+ * Functions included in `evolver` object will not be invoked
+ * if its corresponding key does not exist in the `data` object.
+ * Also, values included in `data` object will not be used
+ * if its corresponding key does not exist in the `evolver` object.
+ *
+ * @param data object whose value is applied to the corresponding function
+ * that is defined in `evolver` at the same path.
+ * @param evolver object that include functions that is applied to
+ * the corresponding value of `data` object at the same path.
+ * @signature
+ *    R.evolve(data, evolver)
+ * @example
+ *    const evolver = {
+ *      count: add(1),
+ *      time: { elapsed: add(1), remaining: add(-1) },
+ *    };
+ *    const data = {
+ *      id: 10,
+ *      count: 10,
+ *      time: { elapsed: 100, remaining: 1400 },
+ *    };
+ *    evolve(data, evolver)
+ *    // => {
+ *    //   id: 10,
+ *    //   count: 11,
+ *    //   time: { elapsed: 101, remaining: 1399 },
+ *    // }
+ * @dataFirst
+ * @category Object
+ */
+export function evolve<T extends object, E extends Evolver<T>>(
+  object: T,
+  evolver: E
+): Evolved<T, E>;
+
+/**
+ * Creates a new object by applying functions that is included in `evolver` object parameter
+ * to the `data` object parameter according to their corresponding path.
+ *
+ * Functions included in `evolver` object will not be invoked
+ * if its corresponding key does not exist in the `data` object.
+ * Also, values included in `data` object will not be used
+ * if its corresponding key does not exist in the `evolver` object.
+ *
+ * @param data object whose value is applied to the corresponding function
+ * that is defined in `evolver` at the same path.
+ * @param evolver object that include functions that is applied to
+ * the corresponding value of `data` object at the same path.
+ * @signature
+ *    R.evolve(evolver)(data)
+ * @example
+ *    const evolver = {
+ *      count: add(1),
+ *      time: { elapsed: add(1), remaining: add(-1) },
+ *    };
+ *    const data = {
+ *      id: 10,
+ *      count: 10,
+ *      time: { elapsed: 100, remaining: 1400 },
+ *    };
+ *    R.pipe(object, R.evolve(evolver))
+ *    // => {
+ *    //   id: 10,
+ *    //   count: 11,
+ *    //   time: { elapsed: 101, remaining: 1399 },
+ *    // }
+ * @dataLast
+ * @category Object
+ */
+export function evolve<T extends object, E extends Evolver<T>>(
+  evolver: E
+): (object: T) => Evolved<T, E>;
+
+export function evolve() {
+  return purry(_evolve, arguments);
+}
+
+function _evolve(data: unknown, evolver: GenericEvolver): unknown {
+  if (typeof data !== 'object' || data === null) {
+    return data;
+  }
+  return toPairs.strict(evolver).reduce<Record<string, unknown>>(
+    (result, [key, value]) => {
+      if (key in result) {
+        result[key] =
+          typeof value === 'function'
+            ? value(result[key])
+            : _evolve(result[key], value);
+      }
+      return result;
+    },
+    { ...data }
+  );
+}
diff --git a/src/index.ts b/src/index.ts
index ea3453f..6feee57 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -21,6 +21,7 @@ export * from './dropLast';
 export * from './dropLastWhile';
 export * from './dropWhile';
 export * from './equals';
+export * from './evolve';
 export * from './filter';
 export * from './find';
 export * from './findIndex';


Test patch:
diff --git a/src/evolve.test.ts b/src/evolve.test.ts
new file mode 100644
index 0000000..db01394
--- /dev/null
+++ b/src/evolve.test.ts
@@ -0,0 +1,403 @@
+import { pipe } from './pipe';
+import { evolve } from './evolve';
+import { omit } from './omit';
+import { set } from './set';
+import { map } from './map';
+import { add } from './add';
+import { reduce } from './reduce';
+
+const sum = reduce((a, b: number) => add(a, b), 0);
+
+describe('data first', () => {
+  it('creates a new object by evolving the `data` according to the `transformation` functions', function () {
+    const evolver = {
+      id: add(1),
+      quartile: sum,
+      time: { elapsed: add(1), remaining: add(-1) },
+    };
+    const data = {
+      id: 1,
+      quartile: [1, 2, 3, 4],
+      time: { elapsed: 100, remaining: 1400 },
+    };
+    const expected = {
+      id: 2,
+      quartile: 10,
+      time: { elapsed: 101, remaining: 1399 },
+    };
+    const result = evolve(data, evolver);
+    expect(result).toEqual(expected);
+    expectTypeOf(result).toEqualTypeOf<typeof expected>();
+  });
+
+  it('does not invoke function if `data` does not contain the key', function () {
+    const data: { id?: number } = {};
+    const expected = {};
+    const evolver = { id: add(1) };
+    const result = evolve(data, evolver);
+    expect(result).toEqual(expected);
+  });
+
+  it('is not destructive and is immutable', function () {
+    const evolver = { n: add(1) };
+    const data = { n: 100 };
+    const expected = { n: 101 };
+    const result = evolve(data, evolver);
+    expect(data).toEqual({ n: 100 });
+    expect(result).toEqual(expected);
+    expect(result).not.toBe(expected);
+    expectTypeOf(result).toEqualTypeOf<typeof expected>();
+  });
+
+  it('is recursive', function () {
+    const evolver = { nested: { second: add(-1), third: add(1) } };
+    const data = { first: 1, nested: { second: 2, third: 3 } };
+    const expected = { first: 1, nested: { second: 1, third: 4 } };
+    const result = evolve(data, evolver);
+    expect(result).toEqual(expected);
+    expectTypeOf(result).toEqualTypeOf<typeof expected>();
+  });
+
+  it('ignores undefined transformations', function () {
+    const evolver = { n: undefined };
+    const data: { n: number } = { n: 0 };
+    const expected = { n: 0 };
+    const result = evolve(data, evolver);
+    expect(result).toEqual(expected);
+  });
+
+  it('can handle data that is complex nested objects', function () {
+    const evolver = {
+      array: (array: ReadonlyArray<string>) => array.length,
+      nestedObj: { a: set<{ b: string }, 'b'>('b', 'Set') },
+      objAry: map(omit<{ a: number; b: number }, 'b'>(['b'])),
+    };
+    const result = evolve(
+      {
+        array: ['1', '2', '3'],
+        nestedObj: { a: { b: 'c' } },
+        objAry: [
+          { a: 0, b: 0 },
+          { a: 1, b: 1 },
+        ],
+      },
+      evolver
+    );
+    const expected = {
+      array: 3,
+      nestedObj: { a: { b: 'Set' } },
+      objAry: [{ a: 0 }, { a: 1 }],
+    };
+    expect(result).toEqual(expected);
+    expectTypeOf(result).toEqualTypeOf<typeof expected>();
+  });
+});
+
+describe('data last', () => {
+  it('creates a new object by evolving the `data` according to the `transformation` functions', function () {
+    const evolver = {
+      id: add(1),
+      quartile: sum,
+      time: { elapsed: add(1), remaining: add(-1) },
+    };
+    const data = {
+      id: 1,
+      quartile: [1, 2, 3, 4],
+      time: { elapsed: 100, remaining: 1400 },
+    };
+    const expected = {
+      id: 2,
+      quartile: 10,
+      time: { elapsed: 101, remaining: 1399 },
+    };
+    const result = pipe(data, evolve(evolver));
+    expect(result).toEqual(expected);
+    expectTypeOf(result).toEqualTypeOf<typeof expected>();
+  });
+
+  it('does not invoke function if `data` does not contain the key', function () {
+    const data: { id?: number } = {};
+    const expected = {};
+    const evolver = { id: add(1) };
+    // const result = pipe(data, evolve(evolver));
+    const result = pipe(data, evolve(evolver));
+    expect(result).toEqual(expected);
+  });
+
+  it('is not destructive and is immutable', function () {
+    const evolver = { n: add(1) };
+    const data = { n: 100 };
+    const expected = { n: 101 };
+    const result = pipe(data, evolve(evolver));
+    expect(data).toEqual({ n: 100 });
+    expect(result).toEqual(expected);
+    expect(result).not.toBe(expected);
+    expectTypeOf(result).toEqualTypeOf<typeof expected>();
+  });
+
+  it('is recursive', function () {
+    const evolver = { nested: { second: add(-1), third: add(1) } };
+    const data = { first: 1, nested: { second: 2, third: 3 } };
+    const expected = { first: 1, nested: { second: 1, third: 4 } };
+    const result = pipe(data, evolve(evolver));
+    expect(result).toEqual(expected);
+    expectTypeOf(result).toEqualTypeOf<typeof expected>();
+  });
+
+  it('ignores undefined transformations', function () {
+    const evolver = { n: undefined };
+    const data = { n: 0 };
+    const expected = { n: 0 };
+    const result = pipe(data, evolve(evolver));
+    expect(result).toEqual(expected);
+    expectTypeOf(result).toEqualTypeOf<typeof expected>();
+  });
+
+  it('can handle data that is complex nested objects', function () {
+    const result = pipe(
+      {
+        array: ['1', '2', '3'],
+        nestedObj: { a: { b: 'c' } },
+        objAry: [
+          { a: 0, b: 0 },
+          { a: 1, b: 1 },
+        ],
+      },
+      evolve({
+        array: (array: Array<string>) => array.length,
+        nestedObj: { a: set<{ b: string }, 'b'>('b', 'Set') },
+        objAry: map(omit<{ a: number; b: number }, 'b'>(['b'])),
+      })
+    );
+    const expected = {
+      array: 3,
+      nestedObj: { a: { b: 'Set' } },
+      objAry: [{ a: 0 }, { a: 1 }],
+    };
+    expect(result).toEqual(expected);
+    expectTypeOf(result).toEqualTypeOf<typeof expected>();
+  });
+});
+
+describe('typing', () => {
+  describe('data first', () => {
+    describe('type reflection', (): void => {
+      interface Data {
+        id: number;
+        quartile: Array<number>;
+        time?: { elapsed: number; remaining?: number };
+      }
+      const data: Data = {
+        id: 1,
+        quartile: [1, 2, 3, 4],
+        time: { elapsed: 100, remaining: 1400 },
+      };
+      const expected = data;
+
+      it('can reflect type of data to function of evolver object', function () {
+        const result = evolve(data, {
+          count: (x: number) => x, // type of parameter is required because `count` property is not defined in data
+          quartile: x => x,
+          time: x => x,
+        });
+        expect(result).toEqual(expected);
+        expectTypeOf(result).toEqualTypeOf<typeof expected>();
+      });
+
+      it('can reflect type of data to function of nested evolver object', function () {
+        const result = evolve(data, {
+          count: (x: number) => x, // type of parameter is required because `count` property is not defined in data
+          quartile: x => x,
+          time: { elapsed: x => x, remaining: x => x },
+        });
+        expect(result).toEqual(expected);
+        expectTypeOf(result).toEqualTypeOf<typeof expected>();
+      });
+    });
+
+    it('can detect mismatch of parameters and arguments', function () {
+      evolve(
+        {
+          number: '1',
+          array: ['1', '2', '3'],
+          nestedObj: { a: { b: 'c' } },
+          objAry: [
+            { a: '0', b: 0 },
+            { a: 1, b: 1 },
+          ],
+        },
+        {
+          // @ts-expect-error -- Type 'string' is not assignable to type 'number'.ts(2322)
+          number: add(1),
+          // @ts-expect-error -- Type 'string' is not assignable to type 'number'.ts(2322)
+          array: (array: ReadonlyArray<number>) => array.length,
+          // @ts-expect-error -- Type 'string' is not assignable to type 'number'.ts(2322)
+          nestedObj: { a: set<{ b: number }, 'b'>('b', 0) },
+          // @ts-expect-error -- Type 'string' is not assignable to type 'number'.ts(2322)
+          objAry: map(omit<{ a: number; b: number }, 'b'>(['b'])),
+        }
+      );
+    });
+
+    it('does not accept the input value that is not Array and Object', function () {
+      const evolver = { a: add(1) };
+      // Type check only
+      () => {
+        // @ts-expect-error -- Argument of type '{ a: (value: number) => number; }' is not assignable to parameter of type 'never'.ts(2345)
+        evolve(0, evolver);
+        // @ts-expect-error -- Argument of type '{ a: (value: number) => number; }' is not assignable to parameter of type 'never'.ts(2345)
+        expect(evolve(undefined, evolver)).toThrowError();
+        // @ts-expect-error -- Argument of type '{ a: (value: number) => number; }' is not assignable to parameter of type 'never'.ts(2345)
+        expect(evolve(null, evolver)).toThrowError();
+        // @ts-expect-error -- Argument of type '{ a: (value: number) => number; }' is not assignable to parameter of type 'never'.ts(2345)
+        expect(evolve('', evolver)).toThrowError();
+      };
+    });
+
+    it('does not accept function that require multiple arguments', function () {
+      evolve(
+        {
+          requiring2Args: 1,
+          requiring3Args: 1,
+        },
+        {
+          // @ts-expect-error -- Target signature provides too few arguments. Expected 2 or more, but got 1.ts(2322)
+          requiring2Args: (a: number, b: number) => a + b,
+          // @ts-expect-error -- Target signature provides too few arguments. Expected 3 or more, but got 1.ts(2322)
+          requiring3Args: (a: number, b: number | undefined, c: number) =>
+            a + (b ?? 0) + c,
+        }
+      );
+    });
+
+    it('accept function whose second and subsequent arguments are optional', function () {
+      const result = evolve(
+        {
+          arg2Optional: 1,
+          arg2arg3Optional: 1,
+        },
+        {
+          arg2Optional: (arg1: number, arg2?: number) => arg2 === undefined,
+          arg2arg3Optional: (arg1: number, arg2?: number, arg3?: number) =>
+            arg2 === undefined && arg3 === undefined,
+        }
+      );
+      expect(result).toEqual({
+        arg2Optional: true,
+        arg2arg3Optional: true,
+      });
+      expectTypeOf(result).toEqualTypeOf<{
+        arg2Optional: boolean;
+        arg2arg3Optional: boolean;
+      }>();
+    });
+
+    it('can not handle function arrays.', function () {
+      evolve(
+        {
+          quartile: [1, 2],
+        },
+        {
+          // @ts-expect-error -- Type '((value: number) => number)[]' provides no match for the signature '(data: number[]): unknown'.ts(2322)
+          quartile: [add(1), add(-1)],
+        }
+      );
+    });
+  });
+
+  describe('data last', () => {
+    describe('it can detect mismatch of parameters and arguments', function () {
+      it('detect property "number" are incompatible', function () {
+        const evolver = {
+          number: add(1),
+          array: (array: ReadonlyArray<number>) => array.length,
+        };
+        pipe(
+          {
+            number: '1',
+            array: ['1', '2', '3'],
+          },
+          // @ts-expect-error -- [ts2345]: Type 'string' is not assignable to type 'number | undefined'.
+          evolve(evolver)
+        );
+      });
+      it('detect property "array" are incompatible', function () {
+        const evolver = {
+          number: add(1),
+          array: (array: ReadonlyArray<number>) => array.length,
+        };
+        pipe(
+          {
+            number: 1,
+            array: ['1', '2', '3'],
+          },
+          // @ts-expect-error -- [ts2345]: Type 'string[]' is not assignable to type 'readonly number[]'.
+          evolve(evolver)
+        );
+      });
+    });
+
+    it('does not accept the input value that is not Array and Object', function () {
+      const evolver = { a: add(1) };
+      // Type check only
+      () => {
+        // @ts-expect-error -- Type '{ a: any; }' provides no match for the signature '(input: number): unknown'.ts(2345)
+        pipe(0, evolve(evolver));
+        // @ts-expect-error -- Type '{ a: any; }' provides no match for the signature '(input: undefined): unknown'.ts(2345)
+        pipe(undefined, evolve(evolver));
+        // @ts-expect-error -- Type '{ a: any; }' provides no match for the signature '(input: null): unknown'.ts(2345)
+        pipe(null, evolve(evolver));
+        // @ts-expect-error -- Type '{ a: any; }' provides no match for the signature '(input: string): unknown'.ts(2345)
+        pipe('', evolve(evolver));
+      };
+    });
+
+    it('does not accept function that require multiple arguments', function () {
+      pipe(
+        {
+          requiring2Args: 1,
+        },
+        // @ts-expect-error -- Type '{ requiring2Args: any; }' provides no match for the signature '(input: { requiring2Args: number; }): unknown'.ts(2345)
+        evolve({
+          // @ts-expect-error -- Target signature provides too few arguments. Expected 2 or more, but got 1.ts(2322)
+          requiring2Args: (a: number, b: number) => a + b,
+        })
+      );
+    });
+
+    it('accept function whose second and subsequent arguments are optional', function () {
+      const result = pipe(
+        {
+          arg2Optional: 1,
+          arg2arg3Optional: 1,
+        },
+        evolve({
+          arg2Optional: (arg1: number, arg2?: number) => arg2 === undefined,
+          arg2arg3Optional: (arg1: number, arg2?: number, arg3?: string) =>
+            arg2 === undefined && arg3 === undefined,
+        })
+      );
+      expect(result).toEqual({
+        arg2Optional: true,
+        arg2arg3Optional: true,
+      });
+      expectTypeOf(result).toEqualTypeOf<{
+        arg2Optional: boolean;
+        arg2arg3Optional: boolean;
+      }>();
+    });
+
+    it('can not handle function arrays.', function () {
+      pipe(
+        {
+          quartile: [1, 2],
+        },
+        // @ts-expect-error -- Type '{ quartile: ((value: number) => number)[]; }' provides no match for the signature '(input: { quartile: number[]; }): unknown'.ts(2345)
+        evolve({
+          // @ts-expect-error -- Type '((value: number) => number)[]' provides no match for the signature '(data: number[]): unknown'.ts(2322)
+          quartile: [add(1), add(-1)],
+        })
+      );
+    });
+  });
+});