Repository: remeda/remeda (ts)

Issue:
Add `findSingle`
A util I've written in several ways already: Return an element if and only if that element is the only one where the predicate is true, and undefined otherwise. `R.findSingle(items, fn)` would be equivalent to:
```ts
R.filter(items, fn).length === 1 ? R.find(items, fn) : undefined
```
but pipeable / with short-circuiting.

Example use cases:
* I have an array of command line options, which should contain exactly one of some list, and I want to show an error if there are none or more than one.
* I have an array that should have a single element I want to get: `R.findSingle(a, () => true)`.

Pull request:
feat(only): Add `only`
closes #467 

i don't think `only` can be made lazy; here's the argument. when called with a value, we either:

* return it immediately as `next`. later if we get another value we can't "take back" the value we gave before
* don't return it as `next`. but then we don't get called again, so there's no way for the caller to get the value

there are changes that can be made to the architecture to allow it to be lazy. the one that needs least changes, i think, is to change `pipe` to be

```diff
    if ((lastLazySeq as any).single) {
-      ret = acc[0];
+      ret = acc[acc.length - 1];
    } else {
      ret = acc;
    }
```

this lets `only` "take back" a value it returned, and replace it with `undefined` (and, surprisingly to me, all tests pass after this change). i don't like it though

---

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>


Patch:
diff --git a/mapping.md b/mapping.md
index 52535c1..8851e57 100644
--- a/mapping.md
+++ b/mapping.md
@@ -17,6 +17,7 @@ documentation when migrating._
 | [`clamp`](https://remedajs.com/docs/#clamp)                         | [`clamp`](https://lodash.com/docs/4.17.15#clamp)                         | [`clamp`](https://ramdajs.com/docs/#clamp)                         |
 | [`clone`](https://remedajs.com/docs/#clone)                         | [`cloneDeep`](https://lodash.com/docs/4.17.15#cloneDeep)                 | [`clone`](https://ramdajs.com/docs/#clone)                         |
 | [`concat`](https://remedajs.com/docs/#concat)                       | [`concat`](https://lodash.com/docs/4.17.15#concat)                       | [`concat`](https://ramdajs.com/docs/#concat)                       |
+| [`conditional`](https://remedajs.com/docs/#conditional)             | [`cond`](https://lodash.com/docs/4.17.15#cond)                           | [`cond`](https://ramdajs.com/docs/#cond)                           |
 | [`createPipe`](https://remedajs.com/docs/#createPipe)               |                                                                          |                                                                    |
 | [`debounce`](https://remedajs.com/docs/#debounce)                   | [`debounce`](https://lodash.com/docs/4.17.15#debounce)                   |                                                                    |
 | [`difference`](https://remedajs.com/docs/#difference)               | [`difference`](https://lodash.com/docs/4.17.15#difference)               | [`difference`](https://ramdajs.com/docs/#difference)               |
@@ -27,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/_purryOn.ts b/src/_purryOn.ts
new file mode 100644
index 0000000..f744cc4
--- /dev/null
+++ b/src/_purryOn.ts
@@ -0,0 +1,16 @@
+/**
+ * Utility for purrying functions based on a predicate for the first argument.
+ *
+ * This is useful for purrying functions with a variadic argument list.
+ */
+export function purryOn<T>(
+  isArg: (firstArg: unknown) => firstArg is T,
+  implementation: (firstArg: T, ...args: Array<any>) => unknown,
+  args: IArguments
+): unknown {
+  const callArgs = Array.from(args);
+  const [dataOrArg, ...rest] = callArgs;
+  return isArg(dataOrArg)
+    ? (data: T) => implementation(data, ...callArgs)
+    : implementation(dataOrArg, ...rest);
+}
diff --git a/src/conditional.ts b/src/conditional.ts
new file mode 100644
index 0000000..24ef6cb
--- /dev/null
+++ b/src/conditional.ts
@@ -0,0 +1,237 @@
+import { purryOn } from './_purryOn';
+
+type Case<In, Out, Thru extends In = In> = readonly [
+  when: ((data: In) => data is Thru) | ((data: In) => boolean),
+  then: (data: Thru) => Out,
+];
+
+/**
+ * Executes a transformer function based on the first matching predicate,
+ * functioning like a series of `if...else if...` statements. It sequentially
+ * evaluates each case and, upon finding a truthy predicate, runs the
+ * corresponding transformer, and returns, ignoring any further cases, even if
+ * they would match.
+ *
+ * !IMPORTANT! - Unlike similar implementations in frameworks like Lodash and
+ * Ramda, the Remeda implementation does **NOT** return a default/fallback
+ * `undefined` value when none of the cases match; and instead will **throw** an
+ * exception in those cases.
+ * To add a default case use the `conditional.defaultCase` helper as the final
+ * case of your implementation. By default it returns `undefined`, but could be
+ * provided a transformer in order to return something else.
+ *
+ * Due to TypeScript's inability to infer the result of negating a type-
+ * predicate we can't refine the types used in subsequent cases based on
+ * previous conditions. Using a `switch (true)` statement or ternary operators
+ * is recommended for more precise type control when such type narrowing is
+ * needed.
+ *
+ * @param data - The input data to be evaluated against the provided cases.
+ * @param cases - A list of (up to 10) tuples, each defining a case. Each tuple
+ * consists of a predicate (or a type-predicate) and a transformer function that
+ * processes the data if its case matches.
+ * @returns The output of the matched transformer. If no cases match, an
+ * exception is thrown. The return type is a union of the return types of all
+ * provided transformers.
+ * @signature
+ *   R.conditional(...cases)(data);
+ * @example
+ *   const nameOrId = 3 as string | number;
+ *   R.pipe(
+ *     nameOrId,
+ *     R.conditional(
+ *       [R.isString, (name) => `Hello ${name}`],
+ *       [R.isNumber, (id) => `Hello ID: ${id}`],
+ *       R.conditional.defaultCase(
+ *         (something) => `Hello something (${JSON.stringify(something)})`,
+ *       ),
+ *     ),
+ *   ); //=> 'Hello ID: 3'
+ * @dataLast
+ * @category Function
+ */
+export function conditional<
+  T,
+  Return0,
+  Return1 = never,
+  Return2 = never,
+  Return3 = never,
+  Return4 = never,
+  Return5 = never,
+  Return6 = never,
+  Return7 = never,
+  Return8 = never,
+  Return9 = never,
+  Thru0 extends T = T,
+  Thru1 extends T = T,
+  Thru2 extends T = T,
+  Thru3 extends T = T,
+  Thru4 extends T = T,
+  Thru5 extends T = T,
+  Thru6 extends T = T,
+  Thru7 extends T = T,
+  Thru8 extends T = T,
+  Thru9 extends T = T,
+>(
+  case0: Case<T, Return0, Thru0>,
+  case1?: Case<T, Return1, Thru1>,
+  case2?: Case<T, Return2, Thru2>,
+  case3?: Case<T, Return3, Thru3>,
+  case4?: Case<T, Return4, Thru4>,
+  case5?: Case<T, Return5, Thru5>,
+  case6?: Case<T, Return6, Thru6>,
+  case7?: Case<T, Return7, Thru7>,
+  case8?: Case<T, Return8, Thru8>,
+  case9?: Case<T, Return9, Thru9>
+): (
+  data: T
+) =>
+  | Return0
+  | Return1
+  | Return2
+  | Return3
+  | Return4
+  | Return5
+  | Return6
+  | Return7
+  | Return8
+  | Return9;
+
+/**
+ * Executes a transformer function based on the first matching predicate,
+ * functioning like a series of `if...else if...` statements. It sequentially
+ * evaluates each case and, upon finding a truthy predicate, runs the
+ * corresponding transformer, and returns, ignoring any further cases, even if
+ * they would match.
+ *
+ * !IMPORTANT! - Unlike similar implementations in frameworks like Lodash and
+ * Ramda, the Remeda implementation does **NOT** return a default/fallback
+ * `undefined` value when none of the cases match; and instead will **throw** an
+ * exception in those cases.
+ * To add a default case use the `conditional.defaultCase` helper as the final
+ * case of your implementation. By default it returns `undefined`, but could be
+ * provided a transformer in order to return something else.
+ *
+ * Due to TypeScript's inability to infer the result of negating a type-
+ * predicate we can't refine the types used in subsequent cases based on
+ * previous conditions. Using a `switch (true)` statement or ternary operators
+ * is recommended for more precise type control when such type narrowing is
+ * needed.
+ *
+ * @param data - The input data to be evaluated against the provided cases.
+ * @param cases - A list of (up to 10) tuples, each defining a case. Each tuple
+ * consists of a predicate (or a type-predicate) and a transformer function that
+ * processes the data if its case matches.
+ * @returns The output of the matched transformer. If no cases match, an
+ * exception is thrown. The return type is a union of the return types of all
+ * provided transformers.
+ * @signature
+ *   R.conditional(data, ...cases);
+ * @example
+ *   const nameOrId = 3 as string | number;
+ *   R.conditional(
+ *     nameOrId,
+ *     [R.isString, (name) => `Hello ${name}`],
+ *     [R.isNumber, (id) => `Hello ID: ${id}`],
+ *     R.conditional.defaultCase(
+ *       (something) => `Hello something (${JSON.stringify(something)})`,
+ *     ),
+ *   ); //=> 'Hello ID: 3'
+ * @dataFirst
+ * @category Function
+ */
+export function conditional<
+  T,
+  Return0,
+  Return1 = never,
+  Return2 = never,
+  Return3 = never,
+  Return4 = never,
+  Return5 = never,
+  Return6 = never,
+  Return7 = never,
+  Return8 = never,
+  Return9 = never,
+  Thru0 extends T = T,
+  Thru1 extends T = T,
+  Thru2 extends T = T,
+  Thru3 extends T = T,
+  Thru4 extends T = T,
+  Thru5 extends T = T,
+  Thru6 extends T = T,
+  Thru7 extends T = T,
+  Thru8 extends T = T,
+  Thru9 extends T = T,
+>(
+  data: T,
+  case0: Case<T, Return0, Thru0>,
+  case1?: Case<T, Return1, Thru1>,
+  case2?: Case<T, Return2, Thru2>,
+  case3?: Case<T, Return3, Thru3>,
+  case4?: Case<T, Return4, Thru4>,
+  case5?: Case<T, Return5, Thru5>,
+  case6?: Case<T, Return6, Thru6>,
+  case7?: Case<T, Return7, Thru7>,
+  case8?: Case<T, Return8, Thru8>,
+  case9?: Case<T, Return9, Thru9>
+):
+  | Return0
+  | Return1
+  | Return2
+  | Return3
+  | Return4
+  | Return5
+  | Return6
+  | Return7
+  | Return8
+  | Return9;
+
+export function conditional(): unknown {
+  return purryOn(isCase, conditionalImplementation, arguments);
+}
+
+function conditionalImplementation<In, Out>(
+  data: In,
+  ...cases: ReadonlyArray<Case<In, Out>>
+): Out {
+  for (const [when, then] of cases) {
+    if (when(data)) {
+      return then(data);
+    }
+  }
+
+  throw new Error('conditional: data failed for all cases');
+}
+
+function isCase(maybeCase: unknown): maybeCase is Case<unknown, unknown> {
+  if (!Array.isArray(maybeCase)) {
+    return false;
+  }
+
+  const [when, then, ...rest] = maybeCase;
+  return (
+    typeof when === 'function' &&
+    when.length <= 1 &&
+    typeof then === 'function' &&
+    then.length <= 1 &&
+    rest.length === 0
+  );
+}
+
+export namespace conditional {
+  /**
+   * A simplified case that accepts all data. Put this as the last case to
+   * prevent an exception from being thrown when none of the previous cases
+   * match.
+   * If this is not the last case it will short-circuit anything after it.
+   * @param then - You only need to provide the transformer, the predicate is
+   * implicit. @default () => undefined, which is how Lodash and Ramda handle
+   * the final fallback case.
+   */
+  export const defaultCase = <In>(
+    then: (data: In) => unknown = trivialDefaultCase
+  ) => [() => true, then] as const;
+}
+
+// Lodash and Ramda return `undefined` as the default case.
+const trivialDefaultCase = () => undefined;
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 1fc4569..9d81d03 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -8,6 +8,7 @@ export * from './clamp';
 export * from './clone';
 export * from './compact';
 export * from './concat';
+export * from './conditional';
 export * from './countBy';
 export * from './createPipe';
 export * from './debounce';
@@ -20,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';
@@ -81,6 +83,7 @@ export * from './objOf';
 export * from './omit';
 export * from './omitBy';
 export * from './once';
+export * from './only';
 export * from './partition';
 export * from './pathOr';
 export * from './pick';
diff --git a/src/only.ts b/src/only.ts
new file mode 100644
index 0000000..049a614
--- /dev/null
+++ b/src/only.ts
@@ -0,0 +1,59 @@
+import { IterableContainer } from './_types';
+import { purry } from './purry';
+
+type Only<T extends IterableContainer> = T extends
+  | readonly []
+  | readonly [unknown, unknown, ...Array<unknown>]
+  | readonly [unknown, ...Array<unknown>, unknown]
+  | readonly [...Array<unknown>, unknown, unknown]
+  ? undefined
+  : T extends readonly [unknown]
+    ? T[number]
+    : T[number] | undefined;
+
+/**
+ * Returns the first and only element of `array`, or undefined otherwise.
+ * Note: In `pipe`, use `only()` form instead of `only`. Otherwise, the
+ * inferred type is lost.
+ * @param array the target array
+ * @signature
+ *    R.only(array)
+ * @example
+ *    R.only([]) // => undefined
+ *    R.only([1]) // => 1
+ *    R.only([1, 2]) // => undefined
+ * @pipeable
+ * @category Array
+ * @dataFirst
+ */
+export function only<T extends IterableContainer>(array: Readonly<T>): Only<T>;
+
+/**
+ * Returns the first and only element of `array`, or undefined otherwise.
+ * Note: In `pipe`, use `only()` form instead of `only`. Otherwise, the
+ * inferred type is lost.
+ * @param array the target array
+ * @signature
+ *    R.only(array)
+ * @example
+ *    R.only([]) // => undefined
+ *    R.only([1]) // => 1
+ *    R.only([1, 2]) // => undefined
+ * @pipeable
+ * @category Array
+ * @dataLast
+ */
+export function only<T extends IterableContainer>(): (
+  array: Readonly<T>
+) => Only<T>;
+
+export function only() {
+  return purry(_only, arguments);
+}
+
+function _only<T>(array: ReadonlyArray<T>) {
+  if (array.length === 1) {
+    return array[0];
+  }
+  return undefined;
+}


Test patch:
diff --git a/src/conditional.test.ts b/src/conditional.test.ts
new file mode 100644
index 0000000..cbc5c64
--- /dev/null
+++ b/src/conditional.test.ts
@@ -0,0 +1,91 @@
+import { conditional } from './conditional';
+import { equals } from './equals';
+import { isNumber } from './isNumber';
+import { isString } from './isString';
+import { pipe } from './pipe';
+
+describe('runtime (dataFirst)', () => {
+  it('falls back to trivial default', () => {
+    expect(conditional('Jokic', conditional.defaultCase())).toBeUndefined();
+  });
+
+  it('falls back to our default', () => {
+    expect(
+      conditional(
+        'Jokic',
+        conditional.defaultCase(() => 'hello')
+      )
+    ).toEqual('hello');
+  });
+
+  it('works with a single case', () => {
+    expect(conditional('Jokic', [equals('Jokic'), () => 'center'])).toBe(
+      'center'
+    );
+  });
+
+  it('works with two cases', () => {
+    expect(
+      conditional(
+        'Jokic',
+        [equals('Murray'), () => 'point guard'],
+        [equals('Jokic'), () => 'center']
+      )
+    ).toBe('center');
+  });
+
+  it('picks the first matching case', () => {
+    expect(
+      conditional(
+        'Jokic',
+        [equals('Jokic'), () => 'center'],
+        [equals('Jokic'), () => 'mvp']
+      )
+    ).toBe('center');
+  });
+
+  it('throws when no matching case', () => {
+    expect(() =>
+      conditional('Jokic', [() => false, () => 'world'])
+    ).toThrowErrorMatchingInlineSnapshot(
+      `[Error: conditional: data failed for all cases]`
+    );
+  });
+});
+
+describe('runtime (dataLast)', () => {
+  test('should return value of first pair', () => {
+    const value = pipe(
+      'Jokic',
+      conditional(
+        [equals('Murray'), () => 'point guard'],
+        [equals('Jokic'), () => 'center'],
+        [equals('Jokic'), () => 'mvp']
+      )
+    );
+    expect(value).toBe('center');
+  });
+});
+
+describe('typing', () => {
+  it('can narrow types in the transformers', () => {
+    const data = 3 as string | number;
+    conditional(
+      data,
+      [
+        isString,
+        str => {
+          expectTypeOf(str).toBeString();
+          expectTypeOf(str).not.toBeNumber();
+        },
+      ],
+      [
+        isNumber,
+        num => {
+          expectTypeOf(num).toBeNumber();
+          expectTypeOf(num).not.toBeString();
+        },
+      ]
+    );
+  });
+});
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)],
+        })
+      );
+    });
+  });
+});
diff --git a/src/only.test.ts b/src/only.test.ts
new file mode 100644
index 0000000..1079c8b
--- /dev/null
+++ b/src/only.test.ts
@@ -0,0 +1,186 @@
+import { only } from './only';
+import { pipe } from './pipe';
+
+describe('data first', () => {
+  test('empty array', () => {
+    expect(only([])).toBeUndefined();
+  });
+
+  test('length 1 array', () => {
+    expect(only([1])).toEqual(1);
+  });
+
+  test('length 2 array', () => {
+    expect(only([1, 2])).toBeUndefined();
+  });
+});
+
+describe('data last', () => {
+  test('empty array', () => {
+    expect(pipe([], only())).toBeUndefined();
+  });
+
+  test('length 1 array', () => {
+    expect(pipe([1], only())).toEqual(1);
+  });
+
+  test('length 2 array', () => {
+    expect(pipe([1, 2], only())).toBeUndefined();
+  });
+});
+
+describe('strict typing', () => {
+  test('simple empty array', () => {
+    const arr: Array<number> = [];
+    const result = only(arr);
+    expectTypeOf(result).toEqualTypeOf<number | undefined>();
+    expect(result).toBeUndefined();
+  });
+
+  test('simple array', () => {
+    const arr: Array<number> = [1];
+    const result = only(arr);
+    expectTypeOf(result).toEqualTypeOf<number | undefined>();
+    expect(result).toEqual(1);
+  });
+
+  test('simple non-empty array', () => {
+    const arr: [number, ...Array<number>] = [1];
+    const result = only(arr);
+    expectTypeOf(result).toEqualTypeOf<number | undefined>();
+    expect(result).toEqual(1);
+  });
+
+  test('simple tuple', () => {
+    const arr: [number, string] = [1, 'a'];
+    const result = only(arr);
+    expectTypeOf(result).toEqualTypeOf<undefined>();
+    expect(result).toBeUndefined();
+  });
+
+  test('array with more than one item', () => {
+    const arr: [number, number, ...Array<number>] = [1, 2];
+    const result = only(arr);
+    expectTypeOf(result).toEqualTypeOf<undefined>();
+    expect(result).toBeUndefined();
+  });
+
+  test('trivial empty array', () => {
+    const arr: [] = [];
+    const result = only(arr);
+    expectTypeOf(result).toEqualTypeOf(undefined);
+    expect(result).toBeUndefined();
+  });
+
+  test('array with last', () => {
+    const arr: [...Array<number>, number] = [1];
+    const result = only(arr);
+    expectTypeOf(result).toEqualTypeOf<number | undefined>();
+    expect(result).toEqual(1);
+  });
+
+  test('tuple with last', () => {
+    const arr: [...Array<string>, number] = ['a', 1];
+    const result = only(arr);
+    expectTypeOf(result).toEqualTypeOf<string | number | undefined>();
+    expect(result).toBeUndefined();
+  });
+
+  test('tuple with two last', () => {
+    const arr: [...Array<string>, number, number] = ['a', 1, 2];
+    const result = only(arr);
+    expectTypeOf(result).toEqualTypeOf<undefined>();
+    expect(result).toBeUndefined();
+  });
+
+  test('tuple with first and last', () => {
+    const arr: [number, ...Array<string>, number] = [1, 'a', 2];
+    const result = only(arr);
+    expectTypeOf(result).toEqualTypeOf<undefined>();
+    expect(result).toBeUndefined();
+  });
+
+  test('tuple with optional and array', () => {
+    const arr: [string?, ...Array<number>] = ['a', 1];
+    const result = only(arr);
+    expectTypeOf(result).toEqualTypeOf<string | number | undefined>();
+    expect(result).toBeUndefined();
+  });
+
+  test('tuple with all optional', () => {
+    const arr: [string?, number?] = ['a', 1];
+    const result = only(arr);
+    expectTypeOf(result).toEqualTypeOf<string | number | undefined>();
+    expect(result).toBeUndefined();
+  });
+
+  test('simple empty readonly array', () => {
+    const arr: ReadonlyArray<number> = [];
+    const result = only(arr);
+    expectTypeOf(result).toEqualTypeOf<number | undefined>();
+    expect(result).toBeUndefined();
+  });
+
+  test('simple readonly array', () => {
+    const arr: ReadonlyArray<number> = [1];
+    const result = only(arr);
+    expectTypeOf(result).toEqualTypeOf<number | undefined>();
+    expect(result).toEqual(1);
+  });
+
+  test('simple non-empty readonly array', () => {
+    const arr: readonly [number, ...Array<number>] = [1];
+    const result = only(arr);
+    expectTypeOf(result).toEqualTypeOf<number | undefined>();
+    expect(result).toEqual(1);
+  });
+
+  test('simple readonly tuple', () => {
+    const arr: readonly [number, string] = [1, 'a'];
+    const result = only(arr);
+    expectTypeOf(result).toEqualTypeOf<undefined>();
+    expect(result).toBeUndefined();
+  });
+
+  test('readonly array with more than one item', () => {
+    const arr: readonly [number, number, ...Array<number>] = [1, 2];
+    const result = only(arr);
+    expectTypeOf(result).toEqualTypeOf<undefined>();
+    expect(result).toBeUndefined();
+  });
+
+  test('readonly trivial empty array', () => {
+    const arr: readonly [] = [];
+    const result = only(arr);
+    expectTypeOf(result).toEqualTypeOf(undefined);
+    expect(result).toBeUndefined();
+  });
+
+  test('readonly array with last', () => {
+    const arr: readonly [...Array<number>, number] = [1];
+    const result = only(arr);
+    expectTypeOf(result).toEqualTypeOf<number | undefined>();
+    expect(result).toEqual(1);
+  });
+
+  test('readonly tuple with last', () => {
+    const arr: readonly [...Array<string>, number] = ['a', 1];
+    const result = only(arr);
+    expectTypeOf(result).toEqualTypeOf<string | number | undefined>();
+    expect(result).toBeUndefined();
+  });
+
+  test('readonly tuple with optional and array', () => {
+    const arr: readonly [string?, ...Array<number>] = ['a', 1];
+    const result = only(arr);
+    expectTypeOf(result).toEqualTypeOf<string | number | undefined>();
+    expect(result).toBeUndefined();
+  });
+
+  test('readonly tuple with all optional', () => {
+    const arr: readonly [string?, number?] = ['a', 1];
+    const result = only(arr);
+    expectTypeOf(result).toEqualTypeOf<string | number | undefined>();
+    expect(result).toBeUndefined();
+  });
+});