Repository: remeda/remeda
Pull request #588 (2024-03-23 13:45:27)

feat: Improve consistency for dataLast/dataFirst usage
Make sure we offer a consistent syntax and convention for dataLast and dataFirst, including making identity dataLast instead of headless.

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/588/builds/486868) or the icon next to each commit SHA.

Latest deployment of this branch, based on commit 7d869cccb7eb348b2bde29cf1272114eeeb7de2b:

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


Patch:
diff --git a/src/constant.ts b/src/constant.ts
index b31ce8f..7de2145 100644
--- a/src/constant.ts
+++ b/src/constant.ts
@@ -7,6 +7,10 @@
  * Notice that this is a dataLast impl where the function needs to be invoked
  * to get the "do nothing" function.
  *
+ * See also:
+ * `doNothing` - A function that doesn't return anything.
+ * `identity` - A function that returns the first argument it receives.
+ *
  * @param value - The constant value that would be returned on every invocation.
  * The value is not copied/cloned on every invocation so care should be taken
  * with mutable objects (like arrays, objects, Maps, etc...).
diff --git a/src/doNothing.ts b/src/doNothing.ts
index c30b6b2..7ea623f 100644
--- a/src/doNothing.ts
+++ b/src/doNothing.ts
@@ -8,6 +8,10 @@
  * Notice that this is a dataLast impl where the function needs to be invoked
  * to get the "do nothing" function.
  *
+ * See also:
+ * * `constant` - A function that ignores it's arguments and returns the same value on every invocation.
+ * * `identity` - A function that returns the first argument it receives.
+ *
  * @signature
  *   R.doNothing();
  * @example
diff --git a/src/identity.ts b/src/identity.ts
index 6fd0bf8..e9b6fb1 100644
--- a/src/identity.ts
+++ b/src/identity.ts
@@ -1,20 +1,20 @@
-// TODO: Adding a native dataLast implementation using `purry` breaks the typing
-// for `identity`. This is caused by the fact that `identity` is **so** generic
-// that it can take in any type of data, including **functions**, making it
-// impossible for typescript to infer the legacy "headless" calls properly,
-// which will definitely break for too many users currently. When we release v2
-// of Remeda we can do these sort of breaking changes.
-
 /**
- * A function that always returns the param passed to it.
+ * A function that returns the first argument passed to it.
+ *
+ * Notice that this is a dataLast impl where the function needs to be invoked
+ * to get the "do nothing" function.
+ *
+ * See also:
+ * * `doNothing` - A function that doesn't return anything.
+ * * `constant` - A function that ignores the input arguments and returns the same value on every invocation.
  *
- * @param value - The param to return.
  * @signature
- *    R.identity(data)
+ *    R.identity();
  * @example
- *    R.identity('foo') // => 'foo'
+ *    R.map([1,2,3], R.identity()); // => [1,2,3]
  * @category Function
  */
-export function identity<T>(value: T): T {
-  return value;
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+export function identity(): <T>(value: T, ...args: any) => T {
+  return (value) => value;
 }
diff --git a/src/prop.ts b/src/prop.ts
index b5b03d6..48cc710 100644
--- a/src/prop.ts
+++ b/src/prop.ts
@@ -1,14 +1,40 @@
+import { purry } from "./purry";
+
 /**
  * Gets the value of the given property.
  *
- * @param propName - The property name.
- * @signature R.prop(prop)(object)
+ * @param data - The object to extract the prop from.
+ * @param key - The key of the property to extract.
+ * @signature
+ *   R.prop(data, key);
+ * @example
+ *   R.prop({ foo: 'bar' }, 'foo'); // => 'bar'
+ * @dataFirst
+ * @category Object
+ */
+export function prop<T, K extends keyof T>(data: T, key: K): T[K];
+
+/**
+ * Gets the value of the given property.
+ *
+ * @param key - The key of the property to extract.
+ * @signature
+ *   R.prop(key)(data);
  * @example
  *    R.pipe({foo: 'bar'}, R.prop('foo')) // => 'bar'
  * @dataLast
  * @category Object
  */
-export const prop =
-  <T, K extends keyof T = keyof T>(propName: K) =>
-  ({ [propName]: value }: T): T[K] =>
-    value;
+export function prop<T, K extends keyof T>(key: K): (data: T) => T[K];
+
+export function prop(): unknown {
+  return purry(propImplementation, arguments);
+}
+
+export function propImplementation<T, K extends keyof T>(
+  data: T,
+  key: K,
+): T[K] {
+  const { [key]: value } = data;
+  return value;
+}
diff --git a/src/randomString.ts b/src/randomString.ts
index c716c80..a6bc53a 100644
--- a/src/randomString.ts
+++ b/src/randomString.ts
@@ -19,6 +19,20 @@ const ALPHABET =
  */
 export function randomString(length: number): string;
 
+/**
+ * Random a non-cryptographic random string from characters a-zA-Z0-9.
+ *
+ * @returns The random string.
+ * @signature
+ *   R.randomString(length)
+ * @example
+ *   R.randomString(5) // => aB92J
+ *   R.pipe(5, R.randomString) // => aB92J
+ * @dataLast
+ * @category String
+ */
+export function randomString(): (length: number) => string;
+
 /**
  * Random a non-cryptographic random string from characters a-zA-Z0-9.
  *
diff --git a/src/sliceString.ts b/src/sliceString.ts
index 69633c5..0eb9793 100644
--- a/src/sliceString.ts
+++ b/src/sliceString.ts
@@ -1,22 +1,55 @@
 /**
- * A data-last version of `String.prototype.slice` so it could be used in pipes.
+ * Extracts a section of this string and returns it as a new string, without
+ * modifying the original string. Equivalent to `String.prototype.slice`.
  *
- * NOTE: You don't need this function if you are calling it directly, just use
- * `String.prototype.slice` directly. This function doesn't provide any type
- * improvements over the built-in types.
+ * @param data - The string to extract from.
+ * @param indexStart - The index of the first character to include in the
+ * returned substring.
+ * @param indexEnd - The index of the first character to exclude from the
+ * returned substring.
+ * @returns A new string containing the extracted section of the string.
+ * @signature
+ *    R.sliceString(data, indexStart, indexEnd)
+ * @example
+ *    R.sliceString("abcdefghijkl", 1) // => `bcdefghijkl`
+ *    R.sliceString("abcdefghijkl", 4, 7) // => `efg`
+ * @dataLast
+ * @category String
+ */
+export function sliceString(
+  data: string,
+  indexStart: number,
+  indexEnd?: number,
+): string;
+
+/**
+ * Extracts a section of this string and returns it as a new string, without
+ * modifying the original string. Equivalent to `String.prototype.slice`.
  *
- * @param indexStart - The index of the first character to include in the returned substring.
- * @param indexEnd - (optional) The index of the first character to exclude from the returned substring.
+ * @param indexStart - The index of the first character to include in the
+ * returned substring.
+ * @param indexEnd - The index of the first character to exclude from the
+ * returned substring, or `undefined` for the rest of the string.
+ * @returns A new string containing the extracted section of the string.
  * @signature
- *    R.sliceString(indexStart)(string)
  *    R.sliceString(indexStart, indexEnd)(string)
  * @example
- *    R.sliceString(1)(`abcdefghijkl`) // => `bcdefghijkl`
- *    R.sliceString(4, 7)(`abcdefghijkl`) // => `efg`
+ *    R.sliceString(1)("abcdefghijkl") // => `bcdefghijkl`
+ *    R.sliceString(4, 7)("abcdefghijkl") // => `efg`
  * @dataLast
  * @category String
  */
-export const sliceString =
-  (indexStart: number, indexEnd?: number): ((data: string) => string) =>
-  (data) =>
-    data.slice(indexStart, indexEnd);
+export function sliceString(
+  indexStart: number,
+  indexEnd?: number,
+): (data: string) => string;
+
+export function sliceString(
+  dataOrIndexStart: number | string,
+  indexStartOrIndexEnd?: number,
+  indexEnd?: number,
+): unknown {
+  return typeof dataOrIndexStart === "string"
+    ? dataOrIndexStart.slice(indexStartOrIndexEnd, indexEnd)
+    : (data: string) => data.slice(dataOrIndexStart, indexStartOrIndexEnd);
+}
diff --git a/src/values.ts b/src/values.ts
index ce36760..eaeb49f 100644
--- a/src/values.ts
+++ b/src/values.ts
@@ -13,13 +13,6 @@ type Values<T extends object> = T extends ReadonlyArray<unknown> | []
  * @example
  *    R.values(['x', 'y', 'z']) // => ['x', 'y', 'z']
  *    R.values({ a: 'x', b: 'y', c: 'z' }) // => ['x', 'y', 'z']
- *    R.pipe(['x', 'y', 'z'], R.values) // => ['x', 'y', 'z']
- *    R.pipe({ a: 'x', b: 'y', c: 'z' }, R.values) // => ['x', 'y', 'z']
- *    R.pipe(
- *      { a: 'x', b: 'y', c: 'z' },
- *      R.values,
- *      R.first,
- *    ) // => 'x'
  * @dataFirst
  * @pipeable
  * @category Object
@@ -43,8 +36,7 @@ export function values<T extends object>(data: T): Values<T>;
  * @pipeable
  * @category Object
  */
-// TODO: Add this back when we deprecate headless calls in V2 of Remeda. Currently the dataLast overload breaks the typing for the headless version of the function, which is used widely in the wild.
-// export function values(): <T extends object>(data: T) => Values<T>;
+export function values(): <T extends object>(data: T) => Values<T>;
 
 export function values(): unknown {
   return purry(Object.values, arguments);


Test patch:
diff --git a/src/debounce.test.ts b/src/debounce.test.ts
index 21a80b2..2d23818 100644
--- a/src/debounce.test.ts
+++ b/src/debounce.test.ts
@@ -3,7 +3,7 @@ import { identity } from "./identity";
 
 describe("Main functionality", () => {
   it("should debounce a function", async () => {
-    const mockFn = vi.fn(identity);
+    const mockFn = vi.fn(identity());
 
     const debouncer = debounce(mockFn, { waitMs: 32 });
 
@@ -30,7 +30,7 @@ describe("Main functionality", () => {
   });
 
   it("subsequent debounced calls return the last `func` result", async () => {
-    const debouncer = debounce(identity, { waitMs: 32 });
+    const debouncer = debounce(identity(), { waitMs: 32 });
     debouncer.call("a");
 
     await sleep(64);
@@ -94,7 +94,7 @@ describe("Main functionality", () => {
   });
 
   it("subsequent leading debounced calls return the last `func` result", async () => {
-    const debouncer = debounce(identity, { waitMs: 32, timing: "leading" });
+    const debouncer = debounce(identity(), { waitMs: 32, timing: "leading" });
 
     expect([debouncer.call("a"), debouncer.call("b")]).toEqual(["a", "a"]);
 
@@ -117,7 +117,7 @@ describe("Main functionality", () => {
 
 describe("Optional param maxWaitMs", () => {
   it("should support a `maxWait` option", async () => {
-    const mockFn = vi.fn(identity);
+    const mockFn = vi.fn(identity());
 
     const debouncer = debounce(mockFn, { waitMs: 32, maxWaitMs: 64 });
 
@@ -211,7 +211,7 @@ describe("Optional param maxWaitMs", () => {
 
 describe("Additional functionality", () => {
   it("can cancel before the timer starts", async () => {
-    const debouncer = debounce(identity, { waitMs: 32 });
+    const debouncer = debounce(identity(), { waitMs: 32 });
     expect(() => {
       debouncer.cancel();
     }).not.toThrow();
@@ -245,7 +245,7 @@ describe("Additional functionality", () => {
   });
 
   it("can cancel after the timer ends", async () => {
-    const debouncer = debounce(identity, { waitMs: 32 });
+    const debouncer = debounce(identity(), { waitMs: 32 });
     expect(debouncer.call("hello")).toBeUndefined();
     await sleep(32);
 
@@ -256,7 +256,7 @@ describe("Additional functionality", () => {
   });
 
   it("can cancel maxWait timer", async () => {
-    const debouncer = debounce(identity, { waitMs: 16, maxWaitMs: 32 });
+    const debouncer = debounce(identity(), { waitMs: 16, maxWaitMs: 32 });
     expect(debouncer.call("hello")).toBeUndefined();
 
     await sleep(1);
@@ -267,14 +267,14 @@ describe("Additional functionality", () => {
   });
 
   it("can return a cached value", () => {
-    const debouncer = debounce(identity, { timing: "leading", waitMs: 32 });
+    const debouncer = debounce(identity(), { timing: "leading", waitMs: 32 });
     expect(debouncer.cachedValue).toBeUndefined();
     expect(debouncer.call("hello")).toEqual("hello");
     expect(debouncer.cachedValue).toEqual("hello");
   });
 
   it("can check for inflight timers (trailing)", async () => {
-    const debouncer = debounce(identity, { waitMs: 32 });
+    const debouncer = debounce(identity(), { waitMs: 32 });
     expect(debouncer.isPending).toEqual(false);
 
     expect(debouncer.call("hello")).toBeUndefined();
@@ -288,7 +288,7 @@ describe("Additional functionality", () => {
   });
 
   it("can check for inflight timers (trailing)", async () => {
-    const debouncer = debounce(identity, { timing: "leading", waitMs: 32 });
+    const debouncer = debounce(identity(), { timing: "leading", waitMs: 32 });
     expect(debouncer.isPending).toEqual(false);
 
     expect(debouncer.call("hello")).toEqual("hello");
@@ -302,7 +302,7 @@ describe("Additional functionality", () => {
   });
 
   it("can flush before a cool-down", async () => {
-    const debouncer = debounce(identity, { waitMs: 32 });
+    const debouncer = debounce(identity(), { waitMs: 32 });
     expect(debouncer.flush()).toBeUndefined();
 
     expect(debouncer.call("hello")).toBeUndefined();
@@ -312,7 +312,7 @@ describe("Additional functionality", () => {
   });
 
   it("can flush during a cool-down", async () => {
-    const debouncer = debounce(identity, { waitMs: 32 });
+    const debouncer = debounce(identity(), { waitMs: 32 });
     expect(debouncer.call("hello")).toBeUndefined();
 
     await sleep(1);
@@ -323,7 +323,7 @@ describe("Additional functionality", () => {
   });
 
   it("can flush after a cool-down", async () => {
-    const debouncer = debounce(identity, { waitMs: 32 });
+    const debouncer = debounce(identity(), { waitMs: 32 });
     expect(debouncer.call("hello")).toBeUndefined();
 
     await sleep(32);
@@ -333,7 +333,7 @@ describe("Additional functionality", () => {
 
 describe("errors", () => {
   it("prevents maxWaitMs to be less then waitMs", () => {
-    expect(() => debounce(identity, { waitMs: 32, maxWaitMs: 16 })).toThrow();
+    expect(() => debounce(identity(), { waitMs: 32, maxWaitMs: 16 })).toThrow();
   });
 });
 
@@ -449,10 +449,10 @@ describe("typing", () => {
   });
 
   it("doesn't accept maxWaitMs when timing is 'leading'", () => {
-    debounce(identity, { timing: "trailing", maxWaitMs: 32 });
-    debounce(identity, { timing: "both", maxWaitMs: 32 });
+    debounce(identity(), { timing: "trailing", maxWaitMs: 32 });
+    debounce(identity(), { timing: "both", maxWaitMs: 32 });
     // @ts-expect-error [ts2769]: maxWaitMs not supported!
-    debounce(identity, { timing: "leading", maxWaitMs: 32 });
+    debounce(identity(), { timing: "leading", maxWaitMs: 32 });
   });
 });
 
diff --git a/src/dropFirstBy.test.ts b/src/dropFirstBy.test.ts
index 79ab948..9a426b9 100644
--- a/src/dropFirstBy.test.ts
+++ b/src/dropFirstBy.test.ts
@@ -5,27 +5,27 @@ import { pipe } from "./pipe";
 describe("runtime (dataFirst)", () => {
   it("works", () => {
     const data = [4, 5, 1, 6, 2, 3, 7];
-    expect(dropFirstBy(data, 2, identity)).toEqual([5, 6, 4, 3, 7]);
+    expect(dropFirstBy(data, 2, identity())).toEqual([5, 6, 4, 3, 7]);
   });
 
   it("handles empty arrays gracefully", () => {
     const data: Array<number> = [];
-    expect(dropFirstBy(data, 1, identity)).toHaveLength(0);
+    expect(dropFirstBy(data, 1, identity())).toHaveLength(0);
   });
 
   it("handles negative numbers gracefully", () => {
     const data = [4, 5, 1, 6, 2, 3, 7];
-    expect(dropFirstBy(data, -3, identity)).toHaveLength(data.length);
+    expect(dropFirstBy(data, -3, identity())).toHaveLength(data.length);
   });
 
   it("handles overflowing numbers gracefully", () => {
     const data = [4, 5, 1, 6, 2, 3, 7];
-    expect(dropFirstBy(data, 100, identity)).toHaveLength(0);
+    expect(dropFirstBy(data, 100, identity())).toHaveLength(0);
   });
 
   it("clones the input when needed", () => {
     const data = [4, 5, 1, 6, 2, 3, 7];
-    const result = dropFirstBy(data, 0, identity);
+    const result = dropFirstBy(data, 0, identity());
     expect(result).not.toBe(data);
     expect(result).toEqual(data);
   });
@@ -43,7 +43,7 @@ describe("runtime (dataFirst)", () => {
       "b",
       "aaaaa",
     ];
-    expect(dropFirstBy(data, 3, (x) => x.length, identity)).toEqual([
+    expect(dropFirstBy(data, 3, (x) => x.length, identity())).toEqual([
       "bbbbb",
       "aaa",
       "bbb",
diff --git a/src/filter.test.ts b/src/filter.test.ts
index 98125ac..fd64ea7 100644
--- a/src/filter.test.ts
+++ b/src/filter.test.ts
@@ -1,5 +1,4 @@
 import { filter } from "./filter";
-import { identity } from "./identity";
 import { map } from "./map";
 import { pipe } from "./pipe";
 
@@ -23,7 +22,7 @@ describe("runtime", () => {
 
   describe("data_last", () => {
     it("filter", () => {
-      const counter = vi.fn(identity);
+      const counter = vi.fn((x: unknown) => x);
       const result = pipe(
         [1, 2, 3],
         filter((x) => x % 2 === 1),
@@ -34,7 +33,7 @@ describe("runtime", () => {
     });
 
     it("filter with typescript guard", () => {
-      const counter = vi.fn(identity);
+      const counter = vi.fn((x: unknown) => x);
       const result = pipe(
         [1, 2, 3, false, "text"],
         filter(isNumber),
@@ -45,7 +44,7 @@ describe("runtime", () => {
     });
 
     it("filter indexed", () => {
-      const counter = vi.fn(identity);
+      const counter = vi.fn((x: unknown) => x);
       const result = pipe(
         [1, 2, 3],
         filter((x, i) => x % 2 === 1 && i !== 1),
diff --git a/src/firstBy.test.ts b/src/firstBy.test.ts
index aa5c34e..ae0aa42 100644
--- a/src/firstBy.test.ts
+++ b/src/firstBy.test.ts
@@ -5,15 +5,15 @@ import { pipe } from "./pipe";
 
 describe("runtime (dataFirst)", () => {
   it("returns undefined on empty", () => {
-    expect(firstBy([], identity)).toBeUndefined();
+    expect(firstBy([], identity())).toBeUndefined();
   });
 
   it("returns the item on a single item array", () => {
-    expect(firstBy([1], identity)).toBe(1);
+    expect(firstBy([1], identity())).toBe(1);
   });
 
   it("finds the minimum", () => {
-    expect(firstBy([2, 1, 4, 3, 5], identity)).toBe(1);
+    expect(firstBy([2, 1, 4, 3, 5], identity())).toBe(1);
   });
 
   it("finds the minimum with a non-trivial order rule", () => {
@@ -23,39 +23,39 @@ describe("runtime (dataFirst)", () => {
   });
 
   it("finds the max with 'desc' order rules", () => {
-    expect(firstBy([2, 1, 4, 3, 5], [identity, "desc"])).toBe(5);
+    expect(firstBy([2, 1, 4, 3, 5], [identity(), "desc"])).toBe(5);
   });
 
   it("finds the max with non-trivial 'desc' order rules", () => {
     expect(
-      firstBy(["aa", "a", "aaaa", "aaa", "aaaaa"], [identity, "desc"]),
+      firstBy(["aa", "a", "aaaa", "aaa", "aaaaa"], [identity(), "desc"]),
     ).toBe("aaaaa");
   });
 
   it("breaks ties with multiple order rules", () => {
     const data = ["a", "bb", "b", "aaaa", "bbb", "aa", "aaa", "bbbb"];
-    expect(firstBy(data, (x) => x.length, identity)).toBe("a");
-    expect(firstBy(data, [(x) => x.length, "desc"], identity)).toBe("aaaa");
-    expect(firstBy(data, (x) => x.length, [identity, "desc"])).toBe("b");
-    expect(firstBy(data, [(x) => x.length, "desc"], [identity, "desc"])).toBe(
+    expect(firstBy(data, (x) => x.length, identity())).toBe("a");
+    expect(firstBy(data, [(x) => x.length, "desc"], identity())).toBe("aaaa");
+    expect(firstBy(data, (x) => x.length, [identity(), "desc"])).toBe("b");
+    expect(firstBy(data, [(x) => x.length, "desc"], [identity(), "desc"])).toBe(
       "bbbb",
     );
   });
 
   it("can compare strings", () => {
-    expect(firstBy(["b", "a", "c"], identity)).toBe("a");
+    expect(firstBy(["b", "a", "c"], identity())).toBe("a");
   });
 
   it("can compare numbers", () => {
-    expect(firstBy([2, 1, 3], identity)).toBe(1);
+    expect(firstBy([2, 1, 3], identity())).toBe(1);
   });
 
   it("can compare booleans", () => {
-    expect(firstBy([true, false, true, true, false], identity)).toBe(false);
+    expect(firstBy([true, false, true, true, false], identity())).toBe(false);
   });
 
   it("can compare valueOfs", () => {
-    expect(firstBy([new Date(), new Date(1), new Date(2)], identity)).toEqual(
+    expect(firstBy([new Date(), new Date(1), new Date(2)], identity())).toEqual(
       new Date(1),
     );
   });
@@ -63,15 +63,15 @@ describe("runtime (dataFirst)", () => {
 
 describe("runtime (dataLast)", () => {
   it("returns undefined on empty", () => {
-    expect(pipe([], firstBy(identity))).toBeUndefined();
+    expect(pipe([], firstBy(identity()))).toBeUndefined();
   });
 
   it("returns the item on a single item array", () => {
-    expect(pipe([1], firstBy(identity))).toBe(1);
+    expect(pipe([1], firstBy(identity()))).toBe(1);
   });
 
   it("finds the minimum", () => {
-    expect(pipe([2, 1, 4, 3, 5], firstBy(identity))).toBe(1);
+    expect(pipe([2, 1, 4, 3, 5], firstBy(identity()))).toBe(1);
   });
 
   it("finds the minimum with a non-trivial order rule", () => {
@@ -84,12 +84,12 @@ describe("runtime (dataLast)", () => {
   });
 
   it("finds the max with 'desc' order rules", () => {
-    expect(pipe([2, 1, 4, 3, 5], firstBy([identity, "desc"]))).toBe(5);
+    expect(pipe([2, 1, 4, 3, 5], firstBy([identity(), "desc"]))).toBe(5);
   });
 
   it("finds the max with non-trivial 'desc' order rules", () => {
     expect(
-      pipe(["aa", "a", "aaaa", "aaa", "aaaaa"], firstBy([identity, "desc"])),
+      pipe(["aa", "a", "aaaa", "aaa", "aaaaa"], firstBy([identity(), "desc"])),
     ).toBe("aaaaa");
   });
 
@@ -98,43 +98,43 @@ describe("runtime (dataLast)", () => {
     expect(
       pipe(
         data,
-        firstBy((x) => x.length, identity),
+        firstBy((x) => x.length, identity()),
       ),
     ).toBe("a");
-    expect(pipe(data, firstBy([(x) => x.length, "desc"], identity))).toBe(
+    expect(pipe(data, firstBy([(x) => x.length, "desc"], identity()))).toBe(
       "aaaa",
     );
     expect(
       pipe(
         data,
-        firstBy((x) => x.length, [identity, "desc"]),
+        firstBy((x) => x.length, [identity(), "desc"]),
       ),
     ).toBe("b");
     expect(
       pipe(
         data,
-        firstBy([(x: string) => x.length, "desc"], [identity, "desc"]),
+        firstBy([(x: string) => x.length, "desc"], [identity(), "desc"]),
       ),
     ).toBe("bbbb");
   });
 
   it("can compare strings", () => {
-    expect(pipe(["b", "a", "c"], firstBy(identity))).toBe("a");
+    expect(pipe(["b", "a", "c"], firstBy(identity()))).toBe("a");
   });
 
   it("can compare numbers", () => {
-    expect(pipe([2, 1, 3], firstBy(identity))).toBe(1);
+    expect(pipe([2, 1, 3], firstBy(identity()))).toBe(1);
   });
 
   it("can compare booleans", () => {
-    expect(pipe([true, false, true, true, false], firstBy(identity))).toBe(
+    expect(pipe([true, false, true, true, false], firstBy(identity()))).toBe(
       false,
     );
   });
 
   it("can compare valueOfs", () => {
     expect(
-      pipe([new Date(), new Date(1), new Date(2)], firstBy(identity)),
+      pipe([new Date(), new Date(1), new Date(2)], firstBy(identity())),
     ).toEqual(new Date(1));
   });
 });
@@ -142,19 +142,19 @@ describe("runtime (dataLast)", () => {
 describe("typing", () => {
   it("can return undefined on arrays", () => {
     const data: ReadonlyArray<number> = [1, 2, 3];
-    const result = firstBy(data, identity);
+    const result = firstBy(data, identity());
     expectTypeOf(result).toBeNullable();
   });
 
   it("can't return undefined on non-empty array", () => {
     const data: NonEmptyArray<number> = [1, 2, 3];
-    const result = firstBy(data, identity);
+    const result = firstBy(data, identity());
     expectTypeOf(result).not.toBeNullable();
   });
 
   it("only returns null on the empty array", () => {
     const data = [] as const;
-    const result = firstBy(data, identity);
+    const result = firstBy(data, identity());
     expectTypeOf(result).toBeUndefined();
   });
 });
diff --git a/src/map.test.ts b/src/map.test.ts
index e54f5dc..adfc804 100644
--- a/src/map.test.ts
+++ b/src/map.test.ts
@@ -105,7 +105,7 @@ describe("runtime", () => {
       123,
       true,
     ];
-    expect(map(input, identity)).toEqual(input);
+    expect(map(input, identity())).toEqual(input);
   });
 
   describe("Indexed", () => {
@@ -204,7 +204,7 @@ describe("typing", () => {
         ...Array<number>,
         boolean,
       ],
-      identity,
+      identity(),
     );
     expectTypeOf(result).toEqualTypeOf<
       [...Array<boolean | number | string>, boolean | number | string]
diff --git a/src/meanBy.test.ts b/src/meanBy.test.ts
index c9979a1..7e9ff76 100644
--- a/src/meanBy.test.ts
+++ b/src/meanBy.test.ts
@@ -20,7 +20,7 @@ describe("data first", () => {
   });
 
   test("should handle empty array", () => {
-    expect(meanBy([], identity)).toBeNaN();
+    expect(meanBy([], identity())).toBeNaN();
   });
 });
 
@@ -44,6 +44,6 @@ describe("data last", () => {
   });
 
   test("should handle empty array", () => {
-    expect(pipe([], meanBy(identity))).toBeNaN();
+    expect(pipe([], meanBy(identity()))).toBeNaN();
   });
 });
diff --git a/src/nthBy.test.ts b/src/nthBy.test.ts
index 5f12902..dfd452e 100644
--- a/src/nthBy.test.ts
+++ b/src/nthBy.test.ts
@@ -5,54 +5,54 @@ import { pipe } from "./pipe";
 describe("runtime (dataFirst)", () => {
   it("works", () => {
     const data = [2, 1, 3];
-    expect(nthBy(data, 0, identity)).toEqual(1);
-    expect(nthBy(data, 1, identity)).toEqual(2);
-    expect(nthBy(data, 2, identity)).toEqual(3);
+    expect(nthBy(data, 0, identity())).toEqual(1);
+    expect(nthBy(data, 1, identity())).toEqual(2);
+    expect(nthBy(data, 2, identity())).toEqual(3);
   });
 
   it("handles negative indexes", () => {
     const data = [2, 1, 3];
-    expect(nthBy(data, -1, identity)).toEqual(3);
-    expect(nthBy(data, -2, identity)).toEqual(2);
-    expect(nthBy(data, -3, identity)).toEqual(1);
+    expect(nthBy(data, -1, identity())).toEqual(3);
+    expect(nthBy(data, -2, identity())).toEqual(2);
+    expect(nthBy(data, -3, identity())).toEqual(1);
   });
 
   it("handles overflows gracefully", () => {
-    expect(nthBy([1, 2, 3], 100, identity)).toBeUndefined();
-    expect(nthBy([1, 2, 3], -100, identity)).toBeUndefined();
+    expect(nthBy([1, 2, 3], 100, identity())).toBeUndefined();
+    expect(nthBy([1, 2, 3], -100, identity())).toBeUndefined();
   });
 
   it("works with complex order rules", () => {
     const data = ["aaaa", "b", "bb", "a", "aaa", "bbbb", "aa", "bbb"] as const;
-    expect(nthBy(data, 0, (a) => a.length, identity)).toEqual("a");
-    expect(nthBy(data, 1, (a) => a.length, identity)).toEqual("b");
-    expect(nthBy(data, 2, (a) => a.length, identity)).toEqual("aa");
-    expect(nthBy(data, 3, (a) => a.length, identity)).toEqual("bb");
-    expect(nthBy(data, 4, (a) => a.length, identity)).toEqual("aaa");
-    expect(nthBy(data, 5, (a) => a.length, identity)).toEqual("bbb");
-    expect(nthBy(data, 6, (a) => a.length, identity)).toEqual("aaaa");
-    expect(nthBy(data, 7, (a) => a.length, identity)).toEqual("bbbb");
+    expect(nthBy(data, 0, (a) => a.length, identity())).toEqual("a");
+    expect(nthBy(data, 1, (a) => a.length, identity())).toEqual("b");
+    expect(nthBy(data, 2, (a) => a.length, identity())).toEqual("aa");
+    expect(nthBy(data, 3, (a) => a.length, identity())).toEqual("bb");
+    expect(nthBy(data, 4, (a) => a.length, identity())).toEqual("aaa");
+    expect(nthBy(data, 5, (a) => a.length, identity())).toEqual("bbb");
+    expect(nthBy(data, 6, (a) => a.length, identity())).toEqual("aaaa");
+    expect(nthBy(data, 7, (a) => a.length, identity())).toEqual("bbbb");
   });
 });
 
 describe("runtime (dataLast)", () => {
   it("works", () => {
     const data = [2, 1, 3];
-    expect(pipe(data, nthBy(0, identity))).toEqual(1);
-    expect(pipe(data, nthBy(1, identity))).toEqual(2);
-    expect(pipe(data, nthBy(2, identity))).toEqual(3);
+    expect(pipe(data, nthBy(0, identity()))).toEqual(1);
+    expect(pipe(data, nthBy(1, identity()))).toEqual(2);
+    expect(pipe(data, nthBy(2, identity()))).toEqual(3);
   });
 
   it("handles negative indexes", () => {
     const data = [2, 1, 3];
-    expect(pipe(data, nthBy(-1, identity))).toEqual(3);
-    expect(pipe(data, nthBy(-2, identity))).toEqual(2);
-    expect(pipe(data, nthBy(-3, identity))).toEqual(1);
+    expect(pipe(data, nthBy(-1, identity()))).toEqual(3);
+    expect(pipe(data, nthBy(-2, identity()))).toEqual(2);
+    expect(pipe(data, nthBy(-3, identity()))).toEqual(1);
   });
 
   it("handles overflows gracefully", () => {
-    expect(pipe([1, 2, 3], nthBy(100, identity))).toBeUndefined();
-    expect(pipe([1, 2, 3], nthBy(-100, identity))).toBeUndefined();
+    expect(pipe([1, 2, 3], nthBy(100, identity()))).toBeUndefined();
+    expect(pipe([1, 2, 3], nthBy(-100, identity()))).toBeUndefined();
   });
 
   it("works with complex order rules", () => {
@@ -60,49 +60,49 @@ describe("runtime (dataLast)", () => {
     expect(
       pipe(
         data,
-        nthBy(0, (a) => a.length, identity),
+        nthBy(0, (a) => a.length, identity()),
       ),
     ).toEqual("a");
     expect(
       pipe(
         data,
-        nthBy(1, (a) => a.length, identity),
+        nthBy(1, (a) => a.length, identity()),
       ),
     ).toEqual("b");
     expect(
       pipe(
         data,
-        nthBy(2, (a) => a.length, identity),
+        nthBy(2, (a) => a.length, identity()),
       ),
     ).toEqual("aa");
     expect(
       pipe(
         data,
-        nthBy(3, (a) => a.length, identity),
+        nthBy(3, (a) => a.length, identity()),
       ),
     ).toEqual("bb");
     expect(
       pipe(
         data,
-        nthBy(4, (a) => a.length, identity),
+        nthBy(4, (a) => a.length, identity()),
       ),
     ).toEqual("aaa");
     expect(
       pipe(
         data,
-        nthBy(5, (a) => a.length, identity),
+        nthBy(5, (a) => a.length, identity()),
       ),
     ).toEqual("bbb");
     expect(
       pipe(
         data,
-        nthBy(6, (a) => a.length, identity),
+        nthBy(6, (a) => a.length, identity()),
       ),
     ).toEqual("aaaa");
     expect(
       pipe(
         data,
-        nthBy(7, (a) => a.length, identity),
+        nthBy(7, (a) => a.length, identity()),
       ),
     ).toEqual("bbbb");
   });
@@ -110,18 +110,18 @@ describe("runtime (dataLast)", () => {
 
 describe("typing", () => {
   it("works with regular arrays", () => {
-    const result = nthBy([1, 2, 3], 0, identity);
+    const result = nthBy([1, 2, 3], 0, identity());
     expectTypeOf(result).toEqualTypeOf<number | undefined>();
   });
 
   it("works with negative indices", () => {
-    const result = nthBy([1, 2, 3], -1, identity);
+    const result = nthBy([1, 2, 3], -1, identity());
     expectTypeOf(result).toEqualTypeOf<number | undefined>();
   });
 
   it("works with tuples", () => {
     const data: [string, boolean, number] = ["a", true, 1];
-    const result = nthBy(data, 1, identity);
+    const result = nthBy(data, 1, identity());
     expectTypeOf(result).toEqualTypeOf<boolean | number | string | undefined>();
   });
 });
diff --git a/src/pipe.test.ts b/src/pipe.test.ts
index b341b33..97387b9 100644
--- a/src/pipe.test.ts
+++ b/src/pipe.test.ts
@@ -104,7 +104,7 @@ describe("lazy", () => {
         return x * 10;
       }),
       take(4),
-      identity,
+      identity(),
       map((x) => {
         count2();
         return x * 10;
diff --git a/src/pullObject.test.ts b/src/pullObject.test.ts
index 2e0f8fb..76aac03 100644
--- a/src/pullObject.test.ts
+++ b/src/pullObject.test.ts
@@ -47,7 +47,7 @@ describe("runtime", () => {
 
     test("number keys", () => {
       expect(
-        pullObject(["a", "aa"], (item) => item.length, identity),
+        pullObject(["a", "aa"], (item) => item.length, identity()),
       ).toStrictEqual({
         1: "a",
         2: "aa",
@@ -55,7 +55,7 @@ describe("runtime", () => {
     });
 
     test("undefined values", () => {
-      expect(pullObject(["a"], identity, constant(undefined))).toStrictEqual({
+      expect(pullObject(["a"], identity(), constant(undefined))).toStrictEqual({
         a: undefined,
       });
     });
@@ -76,8 +76,8 @@ describe("runtime", () => {
     test("Guaranteed to run on each item", () => {
       const data = ["a", "a", "a", "a", "a", "a"];
 
-      const keyFn = vi.fn(identity);
-      const valueFn = vi.fn(identity);
+      const keyFn = vi.fn(identity());
+      const valueFn = vi.fn(identity());
 
       pullObject(data, keyFn, valueFn);
 
@@ -130,14 +130,14 @@ describe("runtime", () => {
       expect(
         pipe(
           ["a", "aa"],
-          pullObject((item) => item.length, identity),
+          pullObject((item) => item.length, identity()),
         ),
       ).toStrictEqual({ 1: "a", 2: "aa" });
     });
 
     test("undefined values", () => {
       expect(
-        pipe(["a"], pullObject(identity, constant(undefined))),
+        pipe(["a"], pullObject(identity(), constant(undefined))),
       ).toStrictEqual({
         a: undefined,
       });
@@ -158,8 +158,8 @@ describe("runtime", () => {
     test("Guaranteed to run on each item", () => {
       const data = ["a", "a", "a", "a", "a", "a"];
 
-      const keyFn = vi.fn(identity);
-      const valueFn = vi.fn(identity);
+      const keyFn = vi.fn(identity());
+      const valueFn = vi.fn(identity());
 
       pipe(data, pullObject(keyFn, valueFn));
 
@@ -173,50 +173,50 @@ describe("typing", () => {
   test("string keys", () => {
     const data = ["a", "b"];
 
-    const dataFirst = pullObject(data, identity, constant("value"));
+    const dataFirst = pullObject(data, identity(), constant("value"));
     expectTypeOf(dataFirst).toEqualTypeOf<Partial<Record<string, string>>>();
 
-    const dataLast = pipe(data, pullObject(identity, constant("value")));
+    const dataLast = pipe(data, pullObject(identity(), constant("value")));
     expectTypeOf(dataLast).toEqualTypeOf<Partial<Record<string, string>>>();
   });
 
   test("number keys", () => {
     const data = [1, 2];
 
-    const dataFirst = pullObject(data, identity, constant(3));
+    const dataFirst = pullObject(data, identity(), constant(3));
     expectTypeOf(dataFirst).toEqualTypeOf<Partial<Record<number, number>>>();
 
-    const dataLast = pipe(data, pullObject(identity, constant(3)));
+    const dataLast = pipe(data, pullObject(identity(), constant(3)));
     expectTypeOf(dataLast).toEqualTypeOf<Partial<Record<number, number>>>();
   });
 
   test("symbol keys", () => {
     const data = [Symbol("a"), Symbol("b")];
 
-    const dataFirst = pullObject(data, identity, constant(Symbol("c")));
+    const dataFirst = pullObject(data, identity(), constant(Symbol("c")));
     expectTypeOf(dataFirst).toEqualTypeOf<Partial<Record<symbol, symbol>>>();
 
-    const dataLast = pipe(data, pullObject(identity, constant(Symbol("c"))));
+    const dataLast = pipe(data, pullObject(identity(), constant(Symbol("c"))));
     expectTypeOf(dataLast).toEqualTypeOf<Partial<Record<symbol, symbol>>>();
   });
 
   test("number constants", () => {
     const data = [1, 2] as const;
 
-    const dataFirst = pullObject(data, identity, constant(3 as const));
+    const dataFirst = pullObject(data, identity(), constant(3 as const));
     expectTypeOf(dataFirst).toEqualTypeOf<Partial<Record<1 | 2, 3>>>();
 
-    const dataLast = pipe(data, pullObject(identity, constant(3 as const)));
+    const dataLast = pipe(data, pullObject(identity(), constant(3 as const)));
     expectTypeOf(dataLast).toEqualTypeOf<Partial<Record<1 | 2, 3>>>();
   });
 
   test("string constants", () => {
     const data = ["a", "b"] as const;
 
-    const dataFirst = pullObject(data, identity, constant("c" as const));
+    const dataFirst = pullObject(data, identity(), constant("c" as const));
     expectTypeOf(dataFirst).toEqualTypeOf<Partial<Record<"a" | "b", "c">>>();
 
-    const dataLast = pipe(data, pullObject(identity, constant("c" as const)));
+    const dataLast = pipe(data, pullObject(identity(), constant("c" as const)));
     expectTypeOf(dataLast).toEqualTypeOf<Partial<Record<"a" | "b", "c">>>();
   });
 
diff --git a/src/rankBy.test.ts b/src/rankBy.test.ts
index 04a1403..aee61bc 100644
--- a/src/rankBy.test.ts
+++ b/src/rankBy.test.ts
@@ -13,19 +13,19 @@ describe("runtime (dataFirst)", () => {
       // TODO: Use `Array.prototype.entries` once we bump our TS target so we
       // can get both the index and the item at the same time, and don't need
       // the non-null assertion.
-      expect(rankBy(data, sorted[i]!, identity)).toBe(i);
+      expect(rankBy(data, sorted[i]!, identity())).toBe(i);
     }
   });
 
   it("can rank items not in the array", () => {
     const data = [5, 1, 3] as const;
-    expect(rankBy(data, 0, identity)).toBe(0);
-    expect(rankBy(data, 2, identity)).toBe(1);
-    expect(rankBy(data, 4, identity)).toBe(2);
+    expect(rankBy(data, 0, identity())).toBe(0);
+    expect(rankBy(data, 2, identity())).toBe(1);
+    expect(rankBy(data, 4, identity())).toBe(2);
   });
 
   it("finds items ranked at the end of the array", () => {
     const data = [5, 1, 3] as const;
-    expect(rankBy(data, 6, identity)).toBe(3);
+    expect(rankBy(data, 6, identity())).toBe(3);
   });
 });
diff --git a/src/takeFirstBy.test.ts b/src/takeFirstBy.test.ts
index 5f4736a..7e8430c 100644
--- a/src/takeFirstBy.test.ts
+++ b/src/takeFirstBy.test.ts
@@ -5,27 +5,27 @@ import { takeFirstBy } from "./takeFirstBy";
 describe("runtime (dataFirst)", () => {
   it("works", () => {
     const data = [4, 5, 1, 6, 2, 3, 7];
-    expect(takeFirstBy(data, 2, identity)).toEqual([2, 1]);
+    expect(takeFirstBy(data, 2, identity())).toEqual([2, 1]);
   });
 
   it("handles empty arrays gracefully", () => {
     const data: Array<number> = [];
-    expect(takeFirstBy(data, 1, identity)).toHaveLength(0);
+    expect(takeFirstBy(data, 1, identity())).toHaveLength(0);
   });
 
   it("handles negative numbers gracefully", () => {
     const data = [4, 5, 1, 6, 2, 3, 7];
-    expect(takeFirstBy(data, -3, identity)).toHaveLength(0);
+    expect(takeFirstBy(data, -3, identity())).toHaveLength(0);
   });
 
   it("handles overflowing numbers gracefully", () => {
     const data = [4, 5, 1, 6, 2, 3, 7];
-    expect(takeFirstBy(data, 100, identity)).toHaveLength(data.length);
+    expect(takeFirstBy(data, 100, identity())).toHaveLength(data.length);
   });
 
   it("clones the array when needed", () => {
     const data = [4, 5, 1, 6, 2, 3, 7];
-    const result = takeFirstBy(data, 100, identity);
+    const result = takeFirstBy(data, 100, identity());
     expect(result).not.toBe(data);
     expect(result).toEqual(data);
   });
@@ -43,7 +43,7 @@ describe("runtime (dataFirst)", () => {
       "b",
       "aaaaa",
     ];
-    expect(takeFirstBy(data, 3, (x) => x.length, identity)).toEqual([
+    expect(takeFirstBy(data, 3, (x) => x.length, identity())).toEqual([
       "aa",
       "b",
       "a",
diff --git a/src/uniqueBy.test.ts b/src/uniqueBy.test.ts
index 71aec27..75732ed 100644
--- a/src/uniqueBy.test.ts
+++ b/src/uniqueBy.test.ts
@@ -16,7 +16,9 @@ describe("uniqueBy", () => {
   ] as const;
 
   it("handles uniq by identity", () => {
-    expect(uniqueBy([1, 2, 2, 5, 1, 6, 7], identity)).toEqual([1, 2, 5, 6, 7]);
+    expect(uniqueBy([1, 2, 2, 5, 1, 6, 7], identity())).toEqual([
+      1, 2, 5, 6, 7,
+    ]);
   });
 
   it("returns people with uniq names", () => {
@@ -54,7 +56,7 @@ describe("uniqueBy", () => {
       const result = pipe(
         [1, 2, 2, 5, 1, 6, 7],
         counter.fn(),
-        uniqueBy(identity),
+        uniqueBy(identity()),
         take(3),
       );
 
@@ -68,7 +70,7 @@ describe("uniqueBy", () => {
         [1, 2, 2, 5, 1, 6, 7],
         counter.fn(),
         take(3),
-        uniqueBy(identity),
+        uniqueBy(identity()),
       );
 
       expect(counter.count).toHaveBeenCalledTimes(3);