Repository: remeda/remeda
Pull request #531 (2024-02-23 10:06:13)

feat: Add native dataLast implementations
It's confusing that some functions have native dataLast impls because of typing, like `first` but some don't. It create weird pipes that don't look consistent.

It also means that our pipe function needs to handle more cases, specifically for lazy invocations where the lazyArgs isn't initialized because it doesn't work via the `purry` function.

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

Latest deployment of this branch, based on commit 9cd1299917b5428fb8f20792d34f1ad2c90be32a:

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

cjquines: i'm confused about which functions this should cover... should this also cover `clone`, `flatten`, `flattenDeep`, `last`, `mergeAll`, `uniq`? should it cover the type guards? should we edit the docs for functions like `only`, `shuffle`, `sort` to have a pipe example for data last?
eranhirsch: It should cover all 1-param functions. Thanks for surfacing others that need to be done.
eranhirsch: This is not a breaking change and the old syntax should still work, there's no way to block it from working, but the old syntax is not well tested (beyond a single test in `first`)
eranhirsch: * Added tests for the "headless" versions
* Went over all functions mentioned here and made sure they all have proper docs.
* Skipping `clone` and `mergeAll` for now, i'm not especially concerned about them and they require a little more care to make sure we don't break anything, specifically mergeAll typing for a dataLast impl might be hard and non-trivial.

Patch:
diff --git a/src/first.ts b/src/first.ts
index c79c95f..db9ee58 100644
--- a/src/first.ts
+++ b/src/first.ts
@@ -1,7 +1,7 @@
 import { IterableContainer } from './_types';
 import { purry } from './purry';
 
-type FirstOut<T extends IterableContainer> = T extends []
+type First<T extends IterableContainer> = T extends []
   ? undefined
   : T extends readonly [unknown, ...Array<unknown>]
     ? T[0]
@@ -11,13 +11,27 @@ type FirstOut<T extends IterableContainer> = T extends []
 
 /**
  * Gets the first element of `array`.
- * Note: In `pipe`, use `first()` form instead of `first`. Otherwise, the inferred type is lost.
- * @param array the array
+ * @param data the array
+ * @returns the first element of the array
  * @signature
  *    R.first(array)
  * @example
  *    R.first([1, 2, 3]) // => 1
  *    R.first([]) // => undefined
+ *
+ * @category Array
+ * @pipeable
+ * @dataFirst
+ */
+export function first<T extends IterableContainer>(data: T): First<T>;
+
+/**
+ * Gets the first element of `array`.
+ * @param data the array
+ * @returns the first element of the array
+ * @signature
+ *    R.first()(array)
+ * @example
  *    R.pipe(
  *      [1, 2, 4, 8, 16],
  *      R.filter(x => x > 3),
@@ -27,13 +41,9 @@ type FirstOut<T extends IterableContainer> = T extends []
  *
  * @category Array
  * @pipeable
+ * @dataLast
  */
-export function first<T extends IterableContainer>(
-  array: Readonly<T>
-): FirstOut<T>;
-export function first<T extends IterableContainer>(): (
-  array: Readonly<T>
-) => FirstOut<T>;
+export function first(): <T extends IterableContainer>(data: T) => First<T>;
 
 export function first() {
   return purry(_first, arguments, first.lazy);
@@ -53,6 +63,7 @@ export namespace first {
       };
     };
   }
+
   export namespace lazy {
     export const single = true;
   }
diff --git a/src/flatten.ts b/src/flatten.ts
index 2b76708..42b063a 100644
--- a/src/flatten.ts
+++ b/src/flatten.ts
@@ -7,21 +7,33 @@ type Flatten<T> = T extends ReadonlyArray<infer K> ? K : T;
 
 /**
  * Flattens `array` a single level deep.
- * Note: In `pipe`, use `flatten()` form instead of `flatten`. Otherwise, the inferred type is lost.
- 
+ *
  * @param items the target array
- * @signature R.flatten(array)
+ * @signature
+ *   R.flatten(array)
  * @example
  *    R.flatten([[1, 2], [3], [4, 5]]) // => [1, 2, 3, 4, 5]
+ * @category Array
+ * @pipeable
+ * @dataFirst
+ */
+export function flatten<T>(items: ReadonlyArray<T>): Array<Flatten<T>>;
+
+/**
+ * Flattens `array` a single level deep.
+ *
+ * @param items the target array
+ * @signature
+ *   R.flatten()(array)
+ * @example
  *    R.pipe(
  *      [[1, 2], [3], [4, 5]],
  *      R.flatten(),
  *    ); // => [1, 2, 3, 4, 5]
  * @category Array
  * @pipeable
+ * @dataLast
  */
-export function flatten<T>(items: ReadonlyArray<T>): Array<Flatten<T>>;
-
 export function flatten<T>(): (items: ReadonlyArray<T>) => Array<Flatten<T>>;
 
 export function flatten() {
diff --git a/src/flattenDeep.ts b/src/flattenDeep.ts
index 74b0ca7..fcfa8ab 100644
--- a/src/flattenDeep.ts
+++ b/src/flattenDeep.ts
@@ -10,20 +10,30 @@ type FlattenDeep4<T> = T extends ReadonlyArray<infer K> ? K : T;
 
 /**
  * Recursively flattens `array`.
- * Note: In `pipe`, use `flattenDeep()` form instead of `flattenDeep`. Otherwise, the inferred type is lost.
  * @param items the target array
- * @signature R.flattenDeep(array)
+ * @signature
+ *   R.flattenDeep(array)
  * @example
  *    R.flattenDeep([[1, 2], [[3], [4, 5]]]) // => [1, 2, 3, 4, 5]
+ * @category Array
+ * @pipeable
+ */
+export function flattenDeep<T>(items: ReadonlyArray<T>): Array<FlattenDeep<T>>;
+
+/**
+ * Recursively flattens `array`.
+ * @param items the target array
+ * @signature
+ *   R.flattenDeep()(array)
+ * @example
  *    R.pipe(
  *      [[1, 2], [[3], [4, 5]]],
  *      R.flattenDeep(),
  *    ); // => [1, 2, 3, 4, 5]
  * @category Array
  * @pipeable
+ * @dataLast
  */
-export function flattenDeep<T>(items: ReadonlyArray<T>): Array<FlattenDeep<T>>;
-
 export function flattenDeep<T>(): (
   items: ReadonlyArray<T>
 ) => Array<FlattenDeep<T>>;
diff --git a/src/fromPairs.ts b/src/fromPairs.ts
index f4352fc..96ecd22 100644
--- a/src/fromPairs.ts
+++ b/src/fromPairs.ts
@@ -1,4 +1,5 @@
 import { IterableContainer } from './_types';
+import { purry } from './purry';
 
 type Entry<Key extends PropertyKey = PropertyKey, Value = unknown> = readonly [
   key: Key,
@@ -21,6 +22,7 @@ type Entry<Key extends PropertyKey = PropertyKey, Value = unknown> = readonly [
  *   R.fromPairs.strict(['a', 1] as const) // => {a: 1} (type: {a: 1})
  * @category Object
  * @strict
+ * @dataFirst
  */
 export function fromPairs<V>(
   pairs: ReadonlyArray<Entry<number, V>>
@@ -29,7 +31,40 @@ export function fromPairs<V>(
   pairs: ReadonlyArray<Entry<string, V>>
 ): Record<string, V>;
 
-export function fromPairs(
+/**
+ * Creates a new object from an array of tuples by pairing up first and second elements as {[key]: value}.
+ * If a tuple is not supplied for any element in the array, the element will be ignored
+ * If duplicate keys exist, the tuple with the greatest index in the input array will be preferred.
+ *
+ * The strict option supports more sophisticated use-cases like those that would
+ * result when calling the strict `toPairs` function.
+ * @param pairs the list of input tuples
+ * @signature
+ *   R.fromPairs()(tuples)
+ *   R.fromPairs.strict()(tuples)
+ * @example
+ *   R.pipe(
+ *     [['a', 'b'], ['c', 'd']],
+ *     R.fromPairs(),
+ *   ); // => {a: 'b', c: 'd'} (type: Record<string, string>)
+ *   R.pipe(
+ *     ['a', 1] as const,
+ *     R.fromPairs.strict(),
+ *   ); // => {a: 1} (type: {a: 1})
+ * @category Object
+ * @strict
+ * @dataLast
+ */
+export function fromPairs(): <K extends PropertyKey, V>(
+  pairs: ReadonlyArray<Entry<K, V>>
+) => Record<K extends string ? string : K extends number ? number : never, V>;
+
+export function fromPairs() {
+  // TODO: When we bump the typescript target beyond ES2019 we can use Object.fromEntries directly here instead of our user-space implementation.
+  return purry(fromPairsImplementation, arguments);
+}
+
+function fromPairsImplementation(
   entries: ReadonlyArray<Entry>
 ): Record<string, unknown> {
   const out: Record<PropertyKey, unknown> = {};
@@ -41,9 +76,14 @@ export function fromPairs(
 
 // Redefining the fromPairs function to allow stricter pairs arrays and fine-
 // grained handling of partiality of the output.
-type Strict = <Entries extends IterableContainer<Entry>>(
-  entries: Entries
-) => StrictOut<Entries>;
+type Strict = {
+  <Entries extends IterableContainer<Entry>>(
+    entries: Entries
+  ): StrictOut<Entries>;
+  (): <Entries extends IterableContainer<Entry>>(
+    entries: Entries
+  ) => StrictOut<Entries>;
+};
 
 // The 2 kinds of arrays we accept result in different kinds of outputs:
 // 1. If the input is a *tuple*, we know exactly what pairs it would hold,
@@ -116,5 +156,5 @@ type ValueForKey<
 
 export namespace fromPairs {
   // Strict is simply a retyping of fromPairs, it runs the same runtime logic.
-  export const strict: Strict = fromPairs;
+  export const strict = fromPairs as Strict;
 }
diff --git a/src/identity.ts b/src/identity.ts
index 8b0559f..b25da0a 100644
--- a/src/identity.ts
+++ b/src/identity.ts
@@ -1,3 +1,10 @@
+// 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
  * @signature
@@ -6,6 +13,6 @@
  *    R.identity('foo') // => 'foo'
  * @category Function
  */
-export function identity<T>(value: T) {
+export function identity<T>(value: T): T {
   return value;
 }
diff --git a/src/keys.ts b/src/keys.ts
index 3de7d0a..39f6d39 100644
--- a/src/keys.ts
+++ b/src/keys.ts
@@ -1,3 +1,6 @@
+import { IterableContainer } from './_types';
+import { purry } from './purry';
+
 /**
  * Returns a new array containing the keys of the array or object.
  * @param source Either an array or an object
@@ -7,26 +10,48 @@
  * @example
  *    R.keys(['x', 'y', 'z']) // => ['0', '1', '2']
  *    R.keys({ a: 'x', b: 'y', c: 'z' }) // => ['a', 'b', 'c']
+ *    R.keys.strict({ a: 'x', b: 'y', 5: 'z' } as const ) // => ['a', 'b', '5'], typed Array<'a' | 'b' | '5'>
+ * @pipeable
+ * @strict
+ * @category Object
+ * @dataFirst
+ */
+export function keys(
+  source: Record<PropertyKey, unknown> | ArrayLike<unknown>
+): Array<string>;
+
+/**
+ * Returns a new array containing the keys of the array or object.
+ * @param source Either an array or an object
+ * @signature
+ *    R.keys()(source)
+ *    R.keys.strict()(source)
+ * @example
+ *    R.pipe(['x', 'y', 'z'], R.keys()) // => ['0', '1', '2']
+ *    R.pipe({ a: 'x', b: 'y', c: 'z' }, R.keys()) // => ['a', 'b', 'c']
  *    R.pipe(
  *      { a: 'x', b: 'y', c: 'z' },
- *      R.keys,
- *      R.first
+ *      R.keys(),
+ *      R.first(),
  *    ) // => 'a'
- *    R.keys.strict({ a: 'x', b: 'y', 5: 'z' } as const ) // => ['a', 'b', '5'], typed Array<'a' | 'b' | '5'>
+ *    R.pipe({ a: 'x', b: 'y', 5: 'z' } as const, R.keys.strict()) // => ['a', 'b', '5'], typed Array<'a' | 'b' | '5'>
  * @pipeable
  * @strict
  * @category Object
+ * @dataLast
  */
-
-import { IterableContainer } from './_types';
-
-export function keys(
+export function keys(): (
   source: Record<PropertyKey, unknown> | ArrayLike<unknown>
-): Array<string> {
-  return Object.keys(source);
+) => Array<string>;
+
+export function keys() {
+  return purry(Object.keys, arguments);
 }
 
-type Strict = <T extends object>(source: T) => Keys<T>;
+type Strict = {
+  <T extends object>(data: T): Keys<T>;
+  (): <T extends object>(data: T) => Keys<T>;
+};
 type Keys<T> = T extends IterableContainer ? ArrayKeys<T> : ObjectKeys<T>;
 
 // The keys output can mirror the input when it is an array/tuple. We do this by
@@ -77,7 +102,5 @@ type ObjectKeys<T> =
     ? []
     : Array<`${Exclude<keyof T, symbol>}`>;
 export namespace keys {
-  // @ts-expect-error [ts2322] - I don't know why i'm getting this, the typing
-  // should be fine here because Key<T> returns a narrower type of string...
-  export const strict: Strict = keys;
+  export const strict = keys as Strict;
 }
diff --git a/src/last.ts b/src/last.ts
index 434e750..116567a 100644
--- a/src/last.ts
+++ b/src/last.ts
@@ -3,26 +3,37 @@ import { purry } from './purry';
 
 /**
  * Gets the last element of `array`.
- * Note: In `pipe`, use `last()` form instead of `last`. Otherwise, the inferred type is lost.
  * @param array the array
  * @signature
  *    R.last(array)
  * @example
  *    R.last([1, 2, 3]) // => 3
  *    R.last([]) // => undefined
+ * @category Array
+ * @pipeable
+ * @dataFirst
+ */
+export function last<T>(array: NonEmptyArray<T>): T;
+export function last<T>(array: ReadonlyArray<T>): T | undefined;
+
+/**
+ * Gets the last element of `array`.
+ * @param array the array
+ * @signature
+ *    R.last()(array)
+ * @example
  *    R.pipe(
  *      [1, 2, 4, 8, 16],
  *      R.filter(x => x > 3),
  *      R.last(),
  *      x => x + 1
  *    ); // => 17
- *
  * @category Array
  * @pipeable
+ * @dataLast
  */
-export function last<T>(array: NonEmptyArray<T>): T;
-export function last<T>(array: ReadonlyArray<T>): T | undefined;
 export function last<T>(): (array: ReadonlyArray<T>) => T | undefined;
+
 export function last() {
   return purry(_last, arguments);
 }
diff --git a/src/length.ts b/src/length.ts
index 46578c5..24e3898 100644
--- a/src/length.ts
+++ b/src/length.ts
@@ -10,8 +10,20 @@ type Enumerable<T> = ArrayLike<T> | Iterable<T>;
  * @example
  *    R.length([1, 2, 3]) // => 3
  * @category Array
+ * @dataFirst
  */
 export function length<T>(items: Enumerable<T>): number;
+
+/**
+ * Counts values of the collection or iterable.
+ * @param items The input data.
+ * @signature
+ *    R.length()(array)
+ * @example
+ *    R.pipe([1, 2, 3], R.length()) // => 3
+ * @category Array
+ * @dataLast
+ */
 export function length<T>(): (items: Enumerable<T>) => number;
 
 /**
diff --git a/src/only.ts b/src/only.ts
index 049a614..cfa7515 100644
--- a/src/only.ts
+++ b/src/only.ts
@@ -13,8 +13,6 @@ type Only<T extends IterableContainer> = T extends
 
 /**
  * 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)
@@ -30,15 +28,13 @@ 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)
+ *    R.only()(array)
  * @example
- *    R.only([]) // => undefined
- *    R.only([1]) // => 1
- *    R.only([1, 2]) // => undefined
+ *    R.pipe([], R.only()); // => undefined
+ *    R.pipe([1], R.only()); // => 1
+ *    R.pipe([1, 2], R.only()); // => undefined
  * @pipeable
  * @category Array
  * @dataLast
diff --git a/src/randomString.ts b/src/randomString.ts
index 4146098..eacea24 100644
--- a/src/randomString.ts
+++ b/src/randomString.ts
@@ -1,18 +1,41 @@
-import { range } from './range';
+import { purry } from './purry';
+import { times } from './times';
+
+const ALPHABET =
+  'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
 
 /**
  * Random a non-cryptographic random string from characters a-zA-Z0-9.
  * @param length the length of the random string
- * @signature randomString(length)
+ * @returns the random string
+ * @signature
+ *   R.randomString(length)
  * @example
- *    randomString(5) // => aB92J
+ *   R.randomString(5) // => aB92J
  * @category String
+ * @dataFirst
  */
-export function randomString(length: number) {
-  const characterSet =
-    'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
-  const randomChar = () =>
-    characterSet[Math.floor(Math.random() * characterSet.length)];
+export function randomString(length: number): string;
 
-  return range(0, length).reduce(text => text + randomChar(), '');
+/**
+ * Random a non-cryptographic random string from characters a-zA-Z0-9.
+ * @param length the length of the random string
+ * @returns the random string
+ * @signature
+ *   R.randomString()(length)
+ * @example
+ *    R.pipe(5, R.randomString()) // => aB92J
+ * @category String
+ * @dataLast
+ */
+export function randomString(): (length: number) => string;
+
+export function randomString() {
+  return purry(randomStringImplementation, arguments);
 }
+
+function randomStringImplementation(length: number) {
+  return times(length, randomChar).join('');
+}
+
+const randomChar = () => ALPHABET[Math.floor(Math.random() * ALPHABET.length)]!;
diff --git a/src/toPairs.ts b/src/toPairs.ts
index 94d00b9..2b372f1 100644
--- a/src/toPairs.ts
+++ b/src/toPairs.ts
@@ -1,3 +1,5 @@
+import { purry } from './purry';
+
 /**
  * Returns an array of key/values of the enumerable properties of an object.
  * @param object
@@ -9,19 +11,44 @@
  *    R.toPairs.strict({ a: 1 } as const) // => [['a', 1]] typed Array<['a', 1]>
  * @strict
  * @category Object
+ * @dataFirst
+ */
+export function toPairs<T>(object: Record<string, T>): Array<[string, T]>;
+
+/**
+ * Returns an array of key/values of the enumerable properties of an object.
+ * @param object
+ * @signature
+ *    R.toPairs()(object)
+ *    R.toPairs.strict()(object)
+ * @example
+ *    R.pipe(
+ *      { a: 1, b: 2, c: 3 },
+ *      toPairs(),
+ *    ); // => [['a', 1], ['b', 2], ['c', 3]]
+ *    R.pipe(
+ *      { a: 1 } as const,
+ *      toPairs.strict(),
+ *    ); // => [['a', 1]] typed Array<['a', 1]>
+ * @strict
+ * @category Object
+ * @dataLast
  */
-export function toPairs<T>(object: Record<string, T>): Array<[string, T]> {
-  return Object.entries(object);
+export function toPairs(): <T>(object: Record<string, T>) => Array<[string, T]>;
+
+export function toPairs() {
+  return purry(Object.entries, arguments);
 }
 
 type Pairs<T> = Array<
   { [K in keyof T]-?: [key: K, value: Required<T>[K]] }[keyof T]
 >;
 
+type Strict = {
+  <T extends NonNullable<unknown>>(object: T): Pairs<T>;
+  (): <T extends NonNullable<unknown>>(object: T) => Pairs<T>;
+};
+
 export namespace toPairs {
-  export function strict<T extends NonNullable<unknown>>(object: T): Pairs<T> {
-    // @ts-expect-error [ts2322] - This is deliberately stricter than what TS
-    // provides out of the box.
-    return Object.entries(object);
-  }
+  export const strict = toPairs as Strict;
 }
diff --git a/src/uniq.ts b/src/uniq.ts
index 287bd0c..cc72c8e 100644
--- a/src/uniq.ts
+++ b/src/uniq.ts
@@ -4,12 +4,24 @@ import { _reduceLazy, LazyResult } from './_reduceLazy';
 /**
  * Returns a new array containing only one copy of each element in the original list.
  * Elements are compared by reference using Set.
- * Note: In `pipe`, use `uniq()` form instead of `uniq`. Otherwise, the inferred type is lost.
  * @param array
  * @signature
  *    R.uniq(array)
  * @example
  *    R.uniq([1, 2, 2, 5, 1, 6, 7]) // => [1, 2, 5, 6, 7]
+ * @pipeable
+ * @category Array
+ * @dataFirst
+ */
+export function uniq<T>(array: ReadonlyArray<T>): Array<T>;
+
+/**
+ * Returns a new array containing only one copy of each element in the original list.
+ * Elements are compared by reference using Set.
+ * @param array
+ * @signature
+ *    R.uniq()(array)
+ * @example
  *    R.pipe(
  *      [1, 2, 2, 5, 1, 6, 7], // only 4 iterations
  *      R.uniq(),
@@ -17,9 +29,8 @@ import { _reduceLazy, LazyResult } from './_reduceLazy';
  *    ) // => [1, 2, 5]
  * @pipeable
  * @category Array
+ * @dataLast
  */
-
-export function uniq<T>(array: ReadonlyArray<T>): Array<T>;
 export function uniq<T>(): (array: ReadonlyArray<T>) => Array<T>;
 
 export function uniq() {
diff --git a/src/values.ts b/src/values.ts
index 7378722..21ef358 100644
--- a/src/values.ts
+++ b/src/values.ts
@@ -1,24 +1,42 @@
+import { purry } from './purry';
+
+type Values<T extends object> = T extends ReadonlyArray<unknown> | []
+  ? Array<T[number]>
+  : Array<T[keyof T]>;
+
 /**
  * Returns a new array containing the values of the array or object.
- * @param source Either an array or an object
+ * @param data Either an array or an object
  * @signature
  *    R.values(source)
  * @example
  *    R.values(['x', 'y', 'z']) // => ['x', 'y', 'z']
  *    R.values({ a: 'x', b: 'y', c: 'z' }) // => ['x', 'y', 'z']
+ * @pipeable
+ * @category Object
+ * @dataFirst
+ */
+export function values<T extends object>(data: T): Values<T>;
+
+/**
+ * Returns a new array containing the values of the array or object.
+ * @param source Either an array or an object
+ * @signature
+ *    R.values()(source)
+ * @example
+ *    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
+ *      R.values(),
+ *      R.first(),
  *    ) // => 'x'
  * @pipeable
  * @category Object
+ * @dataLast
  */
+export function values(): <T extends object>(data: T) => Values<T>;
 
-type Values<T extends object> = T extends ReadonlyArray<unknown> | []
-  ? Array<T[number]>
-  : Array<T[keyof T]>;
-
-export function values<T extends object>(source: T): Values<T> {
-  return Object.values(source) as Values<T>;
+export function values() {
+  return purry(Object.values, arguments);
 }


Test patch:
diff --git a/src/fromPairs.test.ts b/src/fromPairs.test.ts
index 2cab953..590d32f 100644
--- a/src/fromPairs.test.ts
+++ b/src/fromPairs.test.ts
@@ -1,4 +1,5 @@
 import { fromPairs } from './fromPairs';
+import { pipe } from './pipe';
 
 const tuples: Array<[string, number]> = [
   ['a', 1],
@@ -6,14 +7,43 @@ const tuples: Array<[string, number]> = [
   ['c', 3],
 ];
 
-describe('fromPairs', () => {
-  test('generates object from pairs', () => {
+describe('runtime', () => {
+  test('dataFirst', () => {
     expect(fromPairs(tuples)).toEqual({
       a: 1,
       b: 2,
       c: 3,
     });
   });
+
+  test('dataLast', () => {
+    expect(fromPairs()(tuples)).toEqual({
+      a: 1,
+      b: 2,
+      c: 3,
+    });
+  });
+
+  test('dataLast pipe', () => {
+    expect(pipe(tuples, fromPairs())).toEqual({
+      a: 1,
+      b: 2,
+      c: 3,
+    });
+  });
+
+  test('"headless" dataLast', () => {
+    // Older versions of Remeda didn't provide a native dataLast impl and
+    // suggested users use a "headless" version of the dataFirst impl to get the
+    // dataLast behavior.
+    // TODO: Remove this test once we release Remeda v2 where we won't
+    // officially continue to support this.
+    expect(pipe(tuples, fromPairs)).toEqual({
+      a: 1,
+      b: 2,
+      c: 3,
+    });
+  });
 });
 
 describe('typings', () => {
@@ -28,6 +58,12 @@ describe('typings', () => {
     ]);
     assertType<Record<string, string | number>>(actual);
   });
+
+  test('narrow types are widened by non-strict dataLast call', () => {
+    const data = [['a', 1]] as const;
+    expectTypeOf(fromPairs(data)).toEqualTypeOf<Record<string, 1>>();
+    expectTypeOf(pipe(data, fromPairs())).toEqualTypeOf<Record<string, 1>>();
+  });
 });
 
 describe('Strict (with readonly inputs)', () => {
diff --git a/src/keys.test.ts b/src/keys.test.ts
index f49e5af..d8125e0 100644
--- a/src/keys.test.ts
+++ b/src/keys.test.ts
@@ -1,32 +1,61 @@
 import { keys } from './keys';
 import { pipe } from './pipe';
 
-describe('Test for keys', () => {
-  it('should return keys of array', () => {
-    expect(keys(['x', 'y', 'z'])).toEqual(['0', '1', '2']);
-  });
+describe('runtime', () => {
+  describe('dataFirst', () => {
+    it('work with arrays', () => {
+      expect(keys(['x', 'y', 'z'])).toEqual(['0', '1', '2']);
+    });
+
+    it('work with objects', () => {
+      expect(keys({ a: 'x', b: 'y', c: 'z' })).toEqual(['a', 'b', 'c']);
+    });
 
-  it('should return keys of object', () => {
-    expect(keys({ a: 'x', b: 'y', c: 'z' })).toEqual(['a', 'b', 'c']);
+    it('should return strict types', () => {
+      expect(keys.strict({ 5: 'x', b: 'y', c: 'z' } as const)).toEqual([
+        '5',
+        'b',
+        'c',
+      ]);
+    });
   });
 
-  describe('strict', () => {
+  describe('dataLast', () => {
+    it('work with arrays', () => {
+      expect(keys()(['x', 'y', 'z'])).toEqual(['0', '1', '2']);
+    });
+
+    it('work with objects', () => {
+      expect(keys()({ a: 'x', b: 'y', c: 'z' })).toEqual(['a', 'b', 'c']);
+    });
+
     it('should return strict types', () => {
-      const actual = keys.strict({ 5: 'x', b: 'y', c: 'z' } as const);
-      expect(actual).toEqual(['5', 'b', 'c']);
+      expect(keys.strict()({ 5: 'x', b: 'y', c: 'z' } as const)).toEqual([
+        '5',
+        'b',
+        'c',
+      ]);
+    });
 
-      assertType<Array<'5' | 'b' | 'c'>>(actual);
+    it('should work in pipes', () => {
+      expect(pipe({ a: 'x', b: 'y', c: 'z' }, keys())).toEqual(['a', 'b', 'c']);
     });
 
-    it('should work with Partial in pipe', () => {
-      const data: Partial<{ foo: string; bar?: number }> = {
-        foo: '1',
-        bar: 7,
-      };
-      const actual = pipe(data, keys.strict);
-      expect(actual).toEqual(['foo', 'bar']);
+    it('should work in pipes with strict', () => {
+      expect(pipe({ a: 'x', b: 'y', c: 'z' }, keys.strict())).toEqual([
+        'a',
+        'b',
+        'c',
+      ]);
+    });
 
-      assertType<Array<'foo' | 'bar'>>(actual);
+    test('"headless" dataLast', () => {
+      // Older versions of Remeda didn't provide a native dataLast impl and
+      // suggested users use a "headless" version of the dataFirst impl to get the
+      // dataLast behavior.
+      // TODO: Remove this test once we release Remeda v2 where we won't
+      // officially continue to support this.
+      expect(pipe({ a: 'x', b: 'y', c: 'z' }, keys)).toEqual(['a', 'b', 'c']);
     });
   });
 });
diff --git a/src/toPairs.test.ts b/src/toPairs.test.ts
index 8817287..4d4bfcf 100644
--- a/src/toPairs.test.ts
+++ b/src/toPairs.test.ts
@@ -1,9 +1,30 @@
+import { pipe } from './pipe';
 import { toPairs } from './toPairs';
 
-describe('toPairs', () => {
-  test('should return pairs', () => {
-    const actual = toPairs({ a: 1, b: 2, c: 3 });
-    expect(actual).toEqual([
+describe('runtime', () => {
+  test('dataFirst', () => {
+    expect(toPairs({ a: 1, b: 2, c: 3 })).toEqual([
+      ['a', 1],
+      ['b', 2],
+      ['c', 3],
+    ]);
+  });
+
+  test('dataLast', () => {
+    expect(toPairs()({ a: 1, b: 2, c: 3 })).toEqual([
+      ['a', 1],
+      ['b', 2],
+      ['c', 3],
+    ]);
+  });
+
+  test('"headless" dataLast', () => {
+    // Older versions of Remeda didn't provide a native dataLast impl and
+    // suggested users use a "headless" version of the dataFirst impl to get the
+    // dataLast behavior.
+    // TODO: Remove this test once we release Remeda v2 where we won't
+    // officially continue to support this.
+    expect(pipe({ a: 1, b: 2, c: 3 }, toPairs)).toEqual([
       ['a', 1],
       ['b', 2],
       ['c', 3],
diff --git a/src/values.test.ts b/src/values.test.ts
index 5444c6b..f591916 100644
--- a/src/values.test.ts
+++ b/src/values.test.ts
@@ -1,12 +1,43 @@
+import { pipe } from './pipe';
 import { values } from './values';
 
-describe('Test for values as data first', () => {
-  it('should return values of array', () => {
-    expect(values(['x', 'y', 'z'])).toEqual(['x', 'y', 'z']);
+describe('Runtime', () => {
+  describe('dataFirst', () => {
+    it('works with arrays', () => {
+      expect(values(['x', 'y', 'z'])).toEqual(['x', 'y', 'z']);
+    });
+
+    it('should return values of object', () => {
+      expect(values({ a: 'x', b: 'y', c: 'z' })).toEqual(['x', 'y', 'z']);
+    });
   });
 
-  it('should return values of object', () => {
-    expect(values({ a: 'x', b: 'y', c: 'z' })).toEqual(['x', 'y', 'z']);
+  describe('dataLast', () => {
+    it('works with arrays', () => {
+      expect(values()(['x', 'y', 'z'])).toEqual(['x', 'y', 'z']);
+    });
+
+    it('works with objects', () => {
+      expect(values()({ a: 'x', b: 'y', c: 'z' })).toEqual(['x', 'y', 'z']);
+    });
+
+    it('works with pipes', () => {
+      expect(pipe(['x', 'y', 'z'], values())).toEqual(['x', 'y', 'z']);
+      expect(pipe({ a: 'x', b: 'y', c: 'z' }, values())).toEqual([
+        'x',
+        'y',
+        'z',
+      ]);
+    });
+
+    test('"headless" dataLast', () => {
+      // Older versions of Remeda didn't provide a native dataLast impl and
+      // suggested users use a "headless" version of the dataFirst impl to get the
+      // dataLast behavior.
+      // TODO: Remove this test once we release Remeda v2 where we won't
+      // officially continue to support this.
+      expect(pipe({ a: 'x', b: 'y', c: 'z' }, values)).toEqual(['x', 'y', 'z']);
+    });
   });
 });