Repository: remeda/remeda
Pull request #409 (2023-11-23 18:27:28)

feat: Add tap function
This adds the `tap` function, which is useful for performing side effects within a `pipe` sequence, such as logging or persisting the value. The same function exists in Lodash and Ramda.

One annoying thing is that probably the most common use case, `console.log`, breaks the typings when referencing the function directly:
```ts
pipe(
  [-1, 2],
  filter(n => n > 0),
  tap(console.log),
  map(n => n * 2) // 'n' is of type 'unknown'
);
```
This can be avoided by using an arrow function:
```ts
pipe(
  [-1, 2],
  filter(n => n > 0),
  tap(value => console.log(value)),
  map(n => n * 2)
);
```
...or by creating your own log function with a generic type instead of `any`:
```ts
const log = <T>(value: T) => console.log(value);
pipe(
  [-1, 2],
  filter(n => n > 0),
  tap(log),
  map(n => n * 2)
);
```

---

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!
- `fix`: changes to function implementations that fix a bug where a function produces the wrong _runtime_ results.
- `type`: type-only changes (params, overloading, return types, etc...)
- `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/409/builds/441532) or the icon next to each commit SHA.

Latest deployment of this branch, based on commit f311286e5201bd37d901b07fdbd429eb2961bbca:

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

eranhirsch: ```ts
function tap<T, F extends (data: T) => any>(interceptor: F): (data: T) => T;
```

https://www.typescriptlang.org/play?#code/JYWwDg9gTgLgBAbziAhmANHMwwFNMBmwANjLlHAL5wFQQhwBEUuIuAJiowNwBQvBAK4A7AMYxgEYXBhoAPABVMAMTi4AHmWHsAznAAUnWQC44CgJRwAvAD44KYQE8b+4MLJRRuMDGinl5qaGKCZmlrZmiLxwcCwwglDS+gBuKMThdgjRMXBuHl4+0Clp5nw5sbjxiXCpxGVUvJT8vKJSOvDq1lg4uPrZANoAjOgATOgAzAC66NmoYPqdEZ0A1HCD5jMxAPRbMQB6APzZsvOtwjoQxLgAdMQQAOYbs2j6jtZ2b6vrm3A7+0cxE76ABe7zgwNuuGE9xgAAs4F8nttdnBDtkiKRyPoAO5g3F2b7ZP7lNHmIA

This would do the trick. We covered some excellent cases here, so let's ensure the tests represent all of them so it's easy to refactor this in the future.


pekam: Great, thanks!

I started thinking, should we just drop the data first version, since the function is only useful in a pipe (as far as I know), or would you prefer to keep both versions for consistency?

Patch:
diff --git a/mapping.md b/mapping.md
index 72aaa5f..2fc78ee 100644
--- a/mapping.md
+++ b/mapping.md
@@ -85,6 +85,7 @@ documentation when migrating._
 | `swapProps`         | `-`                 | `swap`              |
 | `take`              | `take`              | `take`              |
 | `takeWhile`         | `takeWhile`         | `takeWhile`         |
+| `tap`               | `tap`               | `tap`               |
 | `times`             | `times`             | `times`             |
 | `toPairs`           | `toPairs`           | `toPairs`           |
 | `type`              | `-`                 | `type`              |
diff --git a/src/index.ts b/src/index.ts
index 529be7a..767b592 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -77,7 +77,6 @@ export * from './randomString';
 export * from './range';
 export * from './reduce';
 export * from './reject';
-export * from './splice';
 export * from './reverse';
 export * from './sample';
 export * from './set';
@@ -91,6 +90,7 @@ export * from './sortedIndexBy';
 export * from './sortedIndexWith';
 export * from './sortedLastIndex';
 export * from './sortedLastIndexBy';
+export * from './splice';
 export * from './splitAt';
 export * from './splitWhen';
 export * from './stringToPath';
@@ -99,6 +99,7 @@ export * from './swapIndices';
 export * from './swapProps';
 export * from './take';
 export * from './takeWhile';
+export * from './tap';
 export * from './times';
 export * from './toPairs';
 export * from './type';
diff --git a/src/tap.ts b/src/tap.ts
new file mode 100644
index 0000000..bd5991b
--- /dev/null
+++ b/src/tap.ts
@@ -0,0 +1,50 @@
+import { purry } from './purry';
+
+/**
+ * Calls the given function with the given value, then returns the given value.
+ * The return value of the provided function is ignored.
+ *
+ * This allows "tapping into" a function sequence in a pipe, to perform side
+ * effects on intermediate results.
+ *
+ * @param value the value to pass into the function
+ * @param fn the function to call
+ * @signature
+ *    R.tap(value, fn)
+ * @example
+ *    R.tap("foo", console.log) // => "foo"
+ * @dataFirst
+ * @category Other
+ */
+export function tap<T>(value: T, fn: (value: T) => void): T;
+
+/**
+ * Calls the given function with the given value, then returns the given value.
+ * The return value of the provided function is ignored.
+ *
+ * This allows "tapping into" a function sequence in a pipe, to perform side
+ * effects on intermediate results.
+ *
+ * @param fn the function to call
+ * @signature
+ *    R.tap(fn)(value)
+ * @example
+ *    R.pipe(
+ *      [-5, -1, 2, 3],
+ *      R.filter(n => n > 0),
+ *      R.tap(console.log), // prints [2, 3]
+ *      R.map(n => n * 2)
+ *    ) // => [4, 6]
+ * @dataLast
+ * @category Other
+ */
+export function tap<T, F extends (value: T) => any>(fn: F): (value: T) => T;
+
+export function tap() {
+  return purry(_tap, arguments);
+}
+
+function _tap<T>(value: T, fn: (value: T) => void): T {
+  fn(value);
+  return value;
+}


Test patch:
diff --git a/src/tap.test.ts b/src/tap.test.ts
new file mode 100644
index 0000000..e5a0533
--- /dev/null
+++ b/src/tap.test.ts
@@ -0,0 +1,70 @@
+import { filter } from './filter';
+import { map } from './map';
+import { pipe } from './pipe';
+import { tap } from './tap';
+
+const DATA = [1] as const;
+
+describe('data first', () => {
+  it('should call function with input value', () => {
+    const fn = vi.fn();
+    tap(DATA, fn);
+    expect(fn).toBeCalledWith(DATA);
+    expect(fn).toHaveBeenCalledOnce();
+  });
+
+  it('should return input value', () => {
+    expect(tap(DATA, data => data.length)).toBe(DATA);
+  });
+});
+
+describe('data last', () => {
+  it('should call function with input value', () => {
+    const fn = vi.fn();
+    pipe(DATA, tap(fn));
+    expect(fn).toBeCalledWith(DATA);
+    expect(fn).toHaveBeenCalledOnce();
+  });
+
+  it('should return input value', () => {
+    expect(
+      pipe(
+        DATA,
+        tap(data => data.length)
+      )
+    ).toBe(DATA);
+  });
+
+  it('should work in the middle of pipe sequence', () => {
+    expect(
+      pipe(
+        [-1, 2],
+        filter(n => n > 0),
+        tap(data => {
+          expect(data).toStrictEqual([2]);
+          expectTypeOf(data).toEqualTypeOf<Array<number>>();
+        }),
+        map(n => n * 2)
+      )
+    ).toStrictEqual([4]);
+  });
+
+  it('should infer types after tapping function reference with parameter type any', () => {
+    // (same as console.log)
+    function foo(x: any) {
+      return x;
+    }
+
+    expect(
+      pipe(
+        [-1, 2],
+        filter(n => n > 0),
+        tap(foo),
+        map(n => {
+          expectTypeOf(n).toBeNumber();
+          return n * 2;
+        })
+      )
+    ).toStrictEqual([4]);
+  });
+});