Repository: remeda/remeda
Pull request #623 (2024-04-08 19:36:22)

fix(last): Improve `last` typings for tuples
An attempt to consolidate typings and some naming conventions between `first` and `last`. I tried catching all cases with tests but let me know if you spot a problem that might be solvable.

Would it be beneficial to have a look at typings for `take` and `drop` in a similar manner?

---

Make sure that you:

- [x] Typedoc added for new methods and updated for changed
- [x] Tests added for new methods and updated for changed
- [x] New methods added to `src/index.ts`
- [x] New methods added to `mapping.md`

---

<details><summary>We use semantic PR titles to automate the release process!</summary>

https://conventionalcommits.org

PRs should be titled following using the format: `< TYPE >(< scope >)?: description`

### Available Types:

- `feat`: new functions, and changes to a function's type that would impact users.
- `fix`: changes to the runtime behavior of an existing function, or refinements to it's type that shouldn't impact most users.
- `perf`: changes to function implementations that improve a functions _runtime_ performance.
- `refactor`: changes to function implementations that are neither `fix` nor `perf`
- `test`: tests-only changes (transparent to users of the function).
- `docs`: changes to the documentation of a function **or the documentation site**.
- `build`, `ci`, `style`, `chore`, and `revert`: are only relevant for the internals of the library.

For scope put the name of the function you are working on (either new or
existing).

</details>


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

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

Latest deployment of this branch, based on commit f3d8b1fe13272fe6d6dfcebd0839618b03210924:

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

codecov[bot]: ## [Codecov](https://app.codecov.io/gh/remeda/remeda/pull/623?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=remeda) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Project coverage is 98.59%. Comparing base [(`96a4e82`)](https://app.codecov.io/gh/remeda/remeda/commit/96a4e8276231ba124b6ff3ac6ff997112a56d2d6?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=remeda) to head [(`f3d8b1f`)](https://app.codecov.io/gh/remeda/remeda/pull/623?dropdown=coverage&src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=remeda).
> Report is 78 commits behind head on master.


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


```diff
@@            Coverage Diff             @@
##           master     #623      +/-   ##
==========================================
+ Coverage   98.41%   98.59%   +0.17%     
==========================================
  Files         127      153      +26     
  Lines        7954    10785    +2831     
  Branches      724      879     +155     
==========================================
+ Hits         7828    10633    +2805     
- Misses        123      149      +26     
  Partials        3        3              
```



</details>

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

cjquines: i think this is a good idea, but i would rather we merge a commit into the beta branch, and use the type-fest types `LastArrayElement` for `last` (and possibly `ArraySlice` for `take` and `drop`). wdyt @eranhirsch ?

Patch:
diff --git a/mapping.md b/mapping.md
index 16b344a..7db5748 100644
--- a/mapping.md
+++ b/mapping.md
@@ -35,8 +35,8 @@ documentation when migrating._
 | [`findLastIndex`](https://remedajs.com/docs/#findLastIndex)         | [`findLastIndex`](https://lodash.com/docs/4.17.15#findLastIndex)         | [`findLastIndex`](https://ramdajs.com/docs/#findLastIndex)         |
 | [`first`](https://remedajs.com/docs/#first)                         | [`head`](https://lodash.com/docs/4.17.15#head)                           | [`head`](https://ramdajs.com/docs/#head)                           |
 | [`flatMap`](https://remedajs.com/docs/#flatMap)                     | [`flatMap`](https://lodash.com/docs/4.17.15#flatMap)                     | [`chain`](https://ramdajs.com/docs/#chain)                         |
-| [`flatten`](https://remedajs.com/docs/#flatten)                     | [`flatten`](https://lodash.com/docs/4.17.15#flatten)                     | [`flatten`](https://ramdajs.com/docs/#flatten)                     |
-| [`flattenDeep`](https://remedajs.com/docs/#flattenDeep)             | [`flattenDeep`](https://lodash.com/docs/4.17.15#flattenDeep)             | [`flatten`](https://ramdajs.com/docs/#flatten)                     |
+| [`flat`](https://remedajs.com/docs/#flat)                           | [`flatten`](https://lodash.com/docs/4.17.15#flatten)                     | [`flatten`](https://ramdajs.com/docs/#flatten)                     |
+| [`flat`](https://remedajs.com/docs/#flattenDeep)                    | [`flattenDeep`](https://lodash.com/docs/4.17.15#flattenDeep)             |                                                                    |
 | [`floor`](https://remedajs.com/docs/#floor)                         | [`floor`](https://lodash.com/docs/4.17.15#floor)                         |                                                                    |
 | [`forEach`](https://remedajs.com/docs/#forEach)                     | [`forEach`](https://lodash.com/docs/4.17.15#forEach)                     | [`forEach`](https://ramdajs.com/docs/#forEach)                     |
 | [`forEachObj`](https://remedajs.com/docs/#forEachObj)               | [`forEach`](https://lodash.com/docs/4.17.15#forEach)                     | [`forEachObjIndexed`](https://ramdajs.com/docs/#forEachObjIndexed) |
diff --git a/src/_lazyDataLastImpl.ts b/src/_lazyDataLastImpl.ts
new file mode 100644
index 0000000..b776b25
--- /dev/null
+++ b/src/_lazyDataLastImpl.ts
@@ -0,0 +1,36 @@
+/* eslint-disable @typescript-eslint/no-explicit-any */
+
+import type { LazyEvaluator } from "./pipe";
+
+export type LazyEvaluatorFactory = (...args: any) => LazyEvaluator;
+
+export type MaybeLazyFunction = {
+  (...args: any): unknown;
+  readonly lazy?: LazyEvaluatorFactory;
+};
+
+/**
+ * Use this helper function to build the data last implementation together with
+ * a lazy implementation. Use this when you need to build your own purrying
+ * logic when you want to decide between dataFirst and dataLast on something
+ * that isn't the number of arguments provided. This is useful for implementing
+ * functions with optional or variadic arguments.
+ */
+export function lazyDataLastImpl(
+  fn: MaybeLazyFunction,
+  args: IArguments | ReadonlyArray<unknown>,
+  lazyFactory: LazyEvaluatorFactory | undefined,
+  // TODO: We can probably provide better typing to the return type...
+): unknown {
+  const ret = (data: unknown): unknown =>
+    fn(
+      data,
+      // TODO: Once we bump our target beyond ES5 we can spread the args array directly and don't need this...
+      ...(Array.from(args) as ReadonlyArray<unknown>),
+    );
+
+  const lazy = lazyFactory ?? fn.lazy;
+  return lazy === undefined
+    ? ret
+    : Object.assign(ret, { lazy, lazyArgs: args });
+}
diff --git a/src/flat.ts b/src/flat.ts
new file mode 100644
index 0000000..e9c3d87
--- /dev/null
+++ b/src/flat.ts
@@ -0,0 +1,171 @@
+import { lazyDataLastImpl } from "./_lazyDataLastImpl";
+import type { IterableContainer } from "./_types";
+import type { LazyEvaluator, LazyResult } from "./pipe";
+import type { IsNumericLiteral } from "./type-fest/is-literal";
+
+// This is obvious and not likely to change, but it makes reading the code a
+// little easier as the constant has a name.
+const DEFAULT_DEPTH = 1;
+
+type FlatArray<
+  T,
+  Depth extends number,
+  Iteration extends ReadonlyArray<unknown> = [],
+> = Depth extends Iteration["length"]
+  ? // Stopping condition for the recursion when the array is a tuple.
+    T
+  : T extends readonly []
+    ? // Trivial result when the array is empty.
+      []
+    : T extends readonly [infer Item, ...infer Rest]
+      ? // Tuples could be special-cased by "iterating" over each item
+        // separately so that we maintain more information from the input type,
+        // instead of putting all values in a union.
+        [
+          ...(Item extends IterableContainer
+            ? // If the item itself is an array we continue going deeper
+              FlatArray<Item, Depth, [...Iteration, unknown]>
+            : // But if it isn't we add it to the output tuple
+              [Item]),
+          // And we merge this with the result from the rest of the tuple.
+          ...FlatArray<Rest, Depth, Iteration>,
+        ]
+      : // For simple arrays we compute the item type, and wrap it with an
+        // array.
+        Array<FlatSimpleArrayItems<T, Depth, Iteration>>;
+
+// This type is based on the built-in type for `Array.prototype.flat` from the
+// ES2019 Array typescript library, but we improved it to handle any depth
+// (avoiding the fixed `20` in the built-in type).
+// @see https://github.com/microsoft/TypeScript/blob/main/src/lib/es2019.array.d.ts#L1-L5
+type FlatSimpleArrayItems<
+  T,
+  Depth extends number,
+  Iteration extends ReadonlyArray<unknown> = [],
+  IsDone extends boolean = false,
+> = {
+  done: T;
+  recur: T extends ReadonlyArray<infer InnerArr>
+    ? FlatSimpleArrayItems<
+        InnerArr,
+        Depth,
+        [...Iteration, unknown],
+        // This trick allows us to continue 1 iteration more than the depth,
+        // which is required to flatten the array up to depth.
+        Iteration["length"] extends Depth ? true : false
+      >
+    : T;
+}[IsDone extends true ? "done" : "recur"];
+
+/**
+ * Creates a new array with all sub-array elements concatenated into it
+ * recursively up to the specified depth. Equivalent to the built-in
+ * `Array.prototype.flat` method.
+ *
+ * @param data - The items to flatten.
+ * @param depth - The depth level specifying how deep a nested array structure
+ * should be flattened. Defaults to 1. Non literal values (those typed as
+ * `number`cannot be used. `Infinity`, `Number.POSITIVE_INFINITY` and
+ * `Number.MAX_VALUE` are all typed as `number` and can't be used either. For
+ * "unlimited" depth use a literal value that would exceed your expected
+ * practical maximum nesting level.
+ * @signature
+ *   R.flat(data)
+ *   R.flat(data, depth)
+ * @example
+ *   R.flat([[1, 2], [3, 4], [5], [[6]]]); // => [1, 2, 3, 4, 5, [6]]
+ *   R.flat([[[1]], [[2]]], 2); // => [1, 2]
+ * @dataFirst
+ * @pipeable
+ * @category Array
+ */
+export function flat<
+  T extends IterableContainer,
+  Depth extends number = typeof DEFAULT_DEPTH,
+>(
+  data: T,
+  depth?: IsNumericLiteral<Depth> extends true ? Depth : never,
+): FlatArray<T, Depth>;
+
+/**
+ * Creates a new array with all sub-array elements concatenated into it
+ * recursively up to the specified depth. Equivalent to the built-in
+ * `Array.prototype.flat` method.
+ *
+ * @param depth - The depth level specifying how deep a nested array structure
+ * should be flattened. Defaults to 1.
+ * @signature
+ *   R.flat()(data)
+ *   R.flat(depth)(data)
+ * @example
+ *   R.pipe([[1, 2], [3, 4], [5], [[6]]], R.flat()); // => [1, 2, 3, 4, 5, [6]]
+ *   R.pipe([[[1]], [[2]]], R.flat(2)); // => [1, 2]
+ * @dataLast
+ * @pipeable
+ * @category Array
+ */
+export function flat<Depth extends number = typeof DEFAULT_DEPTH>(
+  depth?: IsNumericLiteral<Depth> extends true ? Depth : never,
+): <T extends IterableContainer>(data: T) => FlatArray<T, Depth>;
+
+export function flat(
+  dataOrDepth?: IterableContainer | number,
+  depth?: number,
+): unknown {
+  if (typeof dataOrDepth === "object") {
+    // TODO: Use the built-in `Array.prototype.flat` in V2.
+    return flatImplementation(dataOrDepth, depth);
+  }
+
+  return lazyDataLastImpl(flatImplementation, arguments, lazyImplementation);
+}
+
+const lazyImplementation = (depth = DEFAULT_DEPTH): LazyEvaluator =>
+  depth <= 0
+    ? lazyIdentity
+    : depth === 1
+      ? lazyShallow
+      : (value) =>
+          Array.isArray(value)
+            ? {
+                next: flatImplementation(value, depth - 1),
+                hasNext: true,
+                hasMany: true,
+                done: false,
+              }
+            : { next: value, hasNext: true, done: false };
+
+// This function is pulled out so that we don't generate a new arrow function
+// each time. It acts as a lazy identity function by wrapping the value with a
+// lazy object.
+const lazyIdentity = <T>(value: T) =>
+  ({ next: value, hasNext: true, done: false }) as const;
+
+// This function is pulled out so that we don't generate a new arrow function
+// each time. Because it doesn't need to run with recursion it could be pulled
+// out from the lazyImplementation and be reused for all invocations.
+const lazyShallow = <T>(value: T): LazyResult<T> =>
+  Array.isArray(value)
+    ? { next: value, hasNext: true, hasMany: true, done: false }
+    : { next: value, hasNext: true, done: false };
+
+function flatImplementation(
+  data: IterableContainer,
+  depth = DEFAULT_DEPTH,
+): IterableContainer {
+  if (depth <= 0) {
+    return data.slice();
+  }
+
+  const output: Array<unknown> = [];
+
+  for (const item of data) {
+    if (Array.isArray(item)) {
+      output.push(...flatImplementation(item, depth - 1));
+    } else {
+      output.push(item);
+    }
+  }
+
+  return output;
+}
diff --git a/src/flatten.ts b/src/flatten.ts
index 68dceed..651e7b8 100644
--- a/src/flatten.ts
+++ b/src/flatten.ts
@@ -7,6 +7,8 @@ type Flatten<T> = T extends ReadonlyArray<infer K> ? K : T;
 /**
  * Flattens `array` a single level deep.
  *
+ * ! **DEPRECATED** Use `R.flat(data)`. Will be removed in V2!
+ *
  * @param items - The target array.
  * @signature
  *   R.flatten(array)
@@ -14,13 +16,16 @@ type Flatten<T> = T extends ReadonlyArray<infer K> ? K : T;
  *    R.flatten([[1, 2], [3], [4, 5]]) // => [1, 2, 3, 4, 5]
  * @dataFirst
  * @pipeable
- * @category Array
+ * @category Deprecated
+ * @deprecated Use `R.flat(data)`. Will be removed in V2!
  */
 export function flatten<T>(items: ReadonlyArray<T>): Array<Flatten<T>>;
 
 /**
  * Flattens `array` a single level deep.
  *
+ * ! **DEPRECATED** Use `R.flat(data)`. Will be removed in V2!
+ *
  * @signature
  *   R.flatten()(array)
  * @example
@@ -30,7 +35,8 @@ export function flatten<T>(items: ReadonlyArray<T>): Array<Flatten<T>>;
  *    ); // => [1, 2, 3, 4, 5]
  * @dataLast
  * @pipeable
- * @category Array
+ * @category Deprecated
+ * @deprecated Use `R.flat()`. Will be removed in V2!
  */
 export function flatten<T>(): (items: ReadonlyArray<T>) => Array<Flatten<T>>;
 
diff --git a/src/flattenDeep.ts b/src/flattenDeep.ts
index fb7bb59..8e67fbc 100644
--- a/src/flattenDeep.ts
+++ b/src/flattenDeep.ts
@@ -12,19 +12,24 @@ type FlattenDeep4<T> = T extends ReadonlyArray<infer K> ? K : T;
 /**
  * Recursively flattens `array`.
  *
+ * ! **DEPRECATED** Use `R.flat(data, 4)`. The typing for `flattenDeep` was broken for arrays nested more than 4 levels deep; this might lead to typing issues when migrating to the new function. Will be removed in V2!
+ *
  * @param items - The target array.
  * @signature
  *   R.flattenDeep(array)
  * @example
  *    R.flattenDeep([[1, 2], [[3], [4, 5]]]) // => [1, 2, 3, 4, 5]
  * @pipeable
- * @category Array
+ * @category Deprecated
+ * @deprecated Use `R.flat(data, 4)`. The typing for `flattenDeep` was broken for arrays nested more than 4 levels deep; this might lead to typing issues when migrating to the new function. Will be removed in V2!
  */
 export function flattenDeep<T>(items: ReadonlyArray<T>): Array<FlattenDeep<T>>;
 
 /**
  * Recursively flattens `array`.
  *
+ * ! **DEPRECATED** Use `R.flat(4)`. The typing for `flattenDeep` was broken for arrays nested more than 4 levels deep; this might lead to typing issues when migrating to the new function. Will be removed in V2!
+ *
  * @signature
  *   R.flattenDeep()(array)
  * @example
@@ -34,7 +39,8 @@ export function flattenDeep<T>(items: ReadonlyArray<T>): Array<FlattenDeep<T>>;
  *    ); // => [1, 2, 3, 4, 5]
  * @dataLast
  * @pipeable
- * @category Array
+ * @category Deprecated
+ * @deprecated Use `R.flat(4)`. The typing for `flattenDeep` was broken for arrays nested more than 4 levels deep; this might lead to typing issues when migrating to the new function. Will be removed in V2!
  */
 export function flattenDeep<T>(): (
   items: ReadonlyArray<T>,
diff --git a/src/index.ts b/src/index.ts
index 629209b..3875585 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -32,6 +32,7 @@ export * from "./findLast";
 export * from "./findLastIndex";
 export * from "./first";
 export * from "./firstBy";
+export * from "./flat";
 export * from "./flatMap";
 export * from "./flatMapToObj";
 export * from "./flatten";
diff --git a/src/last.ts b/src/last.ts
index 769e704..b31a303 100644
--- a/src/last.ts
+++ b/src/last.ts
@@ -1,10 +1,11 @@
-import type { NonEmptyArray } from "./_types";
+import type { IterableContainer } from "./_types";
 import { purry } from "./purry";
+import type { LastArrayElement } from "./type-fest/last-array-element";
 
 /**
  * Gets the last element of `array`.
  *
- * @param array - The array.
+ * @param data - The array.
  * @signature
  *    R.last(array)
  * @example
@@ -14,8 +15,7 @@ import { purry } from "./purry";
  * @pipeable
  * @category Array
  */
-export function last<T>(array: Readonly<NonEmptyArray<T>>): T;
-export function last<T>(array: ReadonlyArray<T>): T | undefined;
+export function last<T extends IterableContainer>(data: T): LastArrayElement<T>;
 
 /**
  * Gets the last element of `array`.
@@ -33,12 +33,16 @@ export function last<T>(array: ReadonlyArray<T>): T | undefined;
  * @pipeable
  * @category Array
  */
-export function last<T>(): (array: ReadonlyArray<T>) => T | undefined;
+export function last<T extends IterableContainer>(): (
+  data: T,
+) => LastArrayElement<T>;
 
 export function last(): unknown {
   return purry(_last, arguments);
 }
 
-function _last<T>(array: ReadonlyArray<T>): T | undefined {
-  return array[array.length - 1];
+function _last<T extends IterableContainer>(
+  data: T,
+): LastArrayElement<T> | undefined {
+  return data[data.length - 1] as LastArrayElement<T> | undefined;
 }
diff --git a/src/pathOr.ts b/src/pathOr.ts
index 26c3aef..d11d38d 100644
--- a/src/pathOr.ts
+++ b/src/pathOr.ts
@@ -69,7 +69,7 @@ type PathValue3<
 > = StrictlyRequired<Pathable2<T, A, B>>[C];
 
 /**
- * Gets the value at `path` of `object`. If the resolved value is `undefined`, the `defaultValue` is returned in its place.
+ * Gets the value at `path` of `object`. If the resolved value is `null` or `undefined`, the `defaultValue` is returned in its place.
  *
  * @param object - The target object.
  * @param path - The path of the property to get.
diff --git a/src/pipe.ts b/src/pipe.ts
index 1655972..dc693f5 100644
--- a/src/pipe.ts
+++ b/src/pipe.ts
@@ -7,7 +7,7 @@ export type LazyEvaluator<T = unknown, R = T> = (
   data?: ReadonlyArray<T>,
 ) => LazyResult<R>;
 
-type LazyResult<T> = LazyEmpty | LazyMany<T> | LazyNext<T>;
+export type LazyResult<T> = LazyEmpty | LazyMany<T> | LazyNext<T>;
 
 type LazyEmpty = {
   done: boolean;
@@ -27,7 +27,7 @@ type LazyMany<T> = {
   done: boolean;
   hasNext: true;
   hasMany: true;
-  next: Array<T>;
+  next: ReadonlyArray<T>;
 };
 
 type PreparedLazyOperation = LazyEvaluator & {
diff --git a/src/purry.ts b/src/purry.ts
index 05dd0f1..6487bd8 100644
--- a/src/purry.ts
+++ b/src/purry.ts
@@ -1,13 +1,8 @@
-/* eslint-disable @typescript-eslint/no-explicit-any */
-
-import type { LazyEvaluator } from "./pipe";
-
-type LazyEvaluatorFactory = (...args: any) => LazyEvaluator;
-
-type MaybeLazyFunction = {
-  (...args: any): unknown;
-  readonly lazy?: LazyEvaluatorFactory;
-};
+import type {
+  LazyEvaluatorFactory,
+  MaybeLazyFunction,
+} from "./_lazyDataLastImpl";
+import { lazyDataLastImpl } from "./_lazyDataLastImpl";
 
 /**
  * Creates a function with `dataFirst` and `dataLast` signatures.
@@ -53,20 +48,15 @@ export function purry(
   args: IArguments | ReadonlyArray<unknown>,
   lazyFactory?: LazyEvaluatorFactory,
 ): unknown {
-  // TODO: Once we bump our target beyond ES5 we can spread the args array directly and don't need this...
-  const callArgs = Array.from(args) as ReadonlyArray<unknown>;
-
   const diff = fn.length - args.length;
+
   if (diff === 0) {
-    return fn(...callArgs);
+    // TODO: Once we bump our target beyond ES5 we can spread the args array directly and don't need this...
+    return fn(...(Array.from(args) as ReadonlyArray<unknown>));
   }
 
   if (diff === 1) {
-    const ret = (data: unknown): unknown => fn(data, ...callArgs);
-    const lazy = lazyFactory ?? fn.lazy;
-    return lazy === undefined
-      ? ret
-      : Object.assign(ret, { lazy, lazyArgs: args });
+    return lazyDataLastImpl(fn, args, lazyFactory);
   }
 
   throw new Error("Wrong number of arguments");
diff --git a/src/type-fest/last-array-element.ts b/src/type-fest/last-array-element.ts
new file mode 100644
index 0000000..4fcf174
--- /dev/null
+++ b/src/type-fest/last-array-element.ts
@@ -0,0 +1,20 @@
+/* eslint-disable @typescript-eslint/no-unused-vars */
+// Returns the last element of an array or tuple
+// Returns `undefined` for empty const tuples
+// Does not add `| undefined` for non-empty const tuples
+export type LastArrayElement<
+  Elements extends ReadonlyArray<unknown>,
+  ElementBeforeTailingSpreadElement = undefined,
+> =
+  // If the last element of an array is a spread element, the `LastArrayElement` result should be `'the type of the element before the spread element' | 'the type of the spread element'`.
+  Elements extends readonly []
+    ? ElementBeforeTailingSpreadElement
+    : Elements extends readonly [...infer _U, infer V]
+      ? V
+      : Elements extends readonly [infer U, ...infer V]
+        ? // If we return `V[number] | U` directly, it would be wrong for `[[string, boolean, object, ...number[]]`.
+          // So we need to recurse type `V` and carry over the type of the element before the spread element.
+          LastArrayElement<V, U>
+        : Elements extends ReadonlyArray<infer U>
+          ? ElementBeforeTailingSpreadElement | U
+          : never;


Test patch:
diff --git a/src/first.test.ts b/src/first.test.ts
index a742a4c..a5ec935 100644
--- a/src/first.test.ts
+++ b/src/first.test.ts
@@ -9,7 +9,7 @@ function defaultTo<T>(d: T) {
   };
 }
 
-test("should return last", () => {
+test("should return first", () => {
   expect(first([1, 2, 3] as const)).toEqual(1);
 });
 
diff --git a/src/flat.test.ts b/src/flat.test.ts
new file mode 100644
index 0000000..2e5c441
--- /dev/null
+++ b/src/flat.test.ts
@@ -0,0 +1,351 @@
+import { createLazyInvocationCounter } from "../test/lazy_invocation_counter";
+import type { NonEmptyArray } from "./_types";
+import { find } from "./find";
+import { flat } from "./flat";
+import { identity } from "./identity";
+import { map } from "./map";
+import { pipe } from "./pipe";
+
+describe("runtime", () => {
+  describe("dataFirst", () => {
+    it("works on empty arrays", () => {
+      expect(flat([], 1)).toStrictEqual([]);
+    });
+
+    it("works on flat arrays", () => {
+      expect(flat([1, 2, 3], 1)).toStrictEqual([1, 2, 3]);
+    });
+
+    it("flattens shallow nested arrays", () => {
+      expect(flat([[1, 2], [3, 4], [5], [6]], 1)).toStrictEqual([
+        1, 2, 3, 4, 5, 6,
+      ]);
+    });
+
+    it("stops at the given depth", () => {
+      expect(flat([[[[[[[[[1]]]]]]]]], 3)).toStrictEqual([[[[[[1]]]]]]);
+    });
+
+    it("works with deeper depth", () => {
+      expect(flat([1], 10)).toStrictEqual([1]);
+    });
+
+    it("handles optional depth as if it was 1", () => {
+      expect(flat([1, [2, 3], [[4]]])).toStrictEqual([1, 2, 3, [4]]);
+    });
+
+    it("handles objects", () => {
+      expect(flat([{ a: 1 }, [{ b: 3 }]], 1)).toStrictEqual([
+        { a: 1 },
+        { b: 3 },
+      ]);
+    });
+
+    it("clones the array on depth 0", () => {
+      const data = [1, 2, 3];
+      const result = flat(data, 0);
+      expect(result).toStrictEqual(data);
+      expect(result).not.toBe(data);
+    });
+
+    it("clones the array when no nested items", () => {
+      const data = [1, 2, 3];
+      const result = flat(data, 1);
+      expect(result).toStrictEqual(data);
+      expect(result).not.toBe(data);
+    });
+  });
+
+  describe("dataLast", () => {
+    it("works on empty arrays", () => {
+      expect(pipe([], flat(1))).toStrictEqual([]);
+    });
+
+    it("works on flat arrays", () => {
+      expect(pipe([1, 2, 3], flat(1))).toStrictEqual([1, 2, 3]);
+    });
+
+    it("flattens shallow nested arrays", () => {
+      expect(pipe([[1, 2], [3, 4], [5], [6]], flat(1))).toStrictEqual([
+        1, 2, 3, 4, 5, 6,
+      ]);
+    });
+
+    it("stops at the given depth", () => {
+      expect(pipe([[[[[[[[[1]]]]]]]]], flat(3))).toStrictEqual([[[[[[1]]]]]]);
+    });
+
+    it("works with deeper depth", () => {
+      expect(pipe([1], flat(10))).toStrictEqual([1]);
+    });
+
+    it("handles optional depth as if it was 1", () => {
+      expect(pipe([1, [2, 3], [[4]]], flat())).toStrictEqual([1, 2, 3, [4]]);
+    });
+
+    it("handles objects", () => {
+      expect(pipe([{ a: 1 }, [{ b: 3 }]], flat(1))).toStrictEqual([
+        { a: 1 },
+        { b: 3 },
+      ]);
+    });
+
+    it("works lazily (shallow)", () => {
+      const beforeMock: <T>(x: T) => T = vi.fn(identity);
+      const afterMock: <T>(x: T) => T = vi.fn(identity);
+      const result = pipe(
+        [[1, 2], 3, [4, 5]],
+        map(beforeMock),
+        flat(1),
+        map(afterMock),
+        find((x) => x - 1 === 2),
+      );
+      expect(beforeMock).toHaveBeenCalledTimes(2);
+      expect(afterMock).toHaveBeenCalledTimes(3);
+      expect(result).toStrictEqual(3);
+    });
+
+    it("works lazily (deep)", () => {
+      const beforeMock: <T>(x: T) => T = vi.fn(identity);
+      const afterMock: <T>(x: T) => T = vi.fn(identity);
+      const result = pipe(
+        [[[0]], [[[1, 2], [[3]], [[4, 5]]]], 6],
+        map(beforeMock),
+        flat(4),
+        map(afterMock),
+        find((x) => x - 1 === 2),
+      );
+      expect(beforeMock).toHaveBeenCalledTimes(2);
+      expect(afterMock).toHaveBeenCalledTimes(4);
+      expect(result).toStrictEqual(3);
+    });
+  });
+
+  it("can go very very deep", () => {
+    expect(
+      flat([[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[1]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]], 99),
+    ).toStrictEqual([1]);
+  });
+});
+
+describe("typing", () => {
+  it("works on empty arrays", () => {
+    const result = flat([], 1);
+    expectTypeOf(result).toEqualTypeOf<[]>();
+  });
+
+  it("works on already-flat arrays", () => {
+    const result = flat([] as Array<string>, 1);
+    expectTypeOf(result).toEqualTypeOf<Array<string>>();
+  });
+
+  it("works on a single level of nesting", () => {
+    const result = flat([] as Array<Array<string>>, 1);
+    expectTypeOf(result).toEqualTypeOf<Array<string>>();
+  });
+
+  it("stops after the first level of nesting (depth === 1)", () => {
+    const result = flat([] as Array<Array<Array<string>>>, 1);
+    expectTypeOf(result).toEqualTypeOf<Array<Array<string>>>();
+  });
+
+  it("stops after the first level of nesting (depth === 1)", () => {
+    const result = flat([] as Array<Array<Array<Array<string>>>>, 1);
+    expectTypeOf(result).toEqualTypeOf<Array<Array<Array<string>>>>();
+  });
+
+  it("works with mixed types", () => {
+    const result = flat([] as Array<Array<number> | Array<string>>, 1);
+    expectTypeOf(result).toEqualTypeOf<Array<number | string>>();
+  });
+
+  it("works with mixed levels of nesting", () => {
+    const result = flat([] as Array<Array<number> | string>, 1);
+    expectTypeOf(result).toEqualTypeOf<Array<number | string>>();
+  });
+
+  it("works when depth is deeper than the array", () => {
+    const result = flat([] as Array<string>, 10);
+    expectTypeOf(result).toEqualTypeOf<Array<string>>();
+  });
+
+  it("works when depth is really really really really big", () => {
+    const result = flat([] as Array<string>, 9_999_999);
+    expectTypeOf(result).toEqualTypeOf<Array<string>>();
+  });
+
+  it("keeps the typing of trivial tuples", () => {
+    const result = flat([1, 2] as const, 1);
+    expectTypeOf(result).toEqualTypeOf<[1, 2]>();
+  });
+
+  it("Works on simple tuples", () => {
+    const result = flat(
+      [
+        [1, 2],
+        [3, 4],
+      ] as const,
+      1,
+    );
+    expectTypeOf(result).toEqualTypeOf<[1, 2, 3, 4]>();
+  });
+
+  it("works on tuples with different levels of nesting", () => {
+    const result = flat([1, [2, 3], [4, [5, 6]]] as const, 1);
+    expectTypeOf(result).toEqualTypeOf<[1, 2, 3, 4, readonly [5, 6]]>();
+  });
+
+  it("works on tuples with depth>1", () => {
+    const result = flat([1, [2, 3], [4, [5, 6]]] as const, 2);
+    expectTypeOf(result).toEqualTypeOf<[1, 2, 3, 4, 5, 6]>();
+  });
+
+  it("works with tuples with a lot of nesting", () => {
+    const result = flat([[[[1]], [[[[2]]]]], [[[[3, 4], 5]]]] as const, 10);
+    expectTypeOf(result).toEqualTypeOf<[1, 2, 3, 4, 5]>();
+  });
+
+  it("works with a mix of simple arrays and tuples", () => {
+    const result = flat([[]] as [Array<string>], 1);
+    expectTypeOf(result).toEqualTypeOf<Array<string>>();
+  });
+
+  it("works with multiple types nested in a tuple", () => {
+    const result = flat([[], []] as [Array<string>, Array<number>], 1);
+    expectTypeOf(result).toEqualTypeOf<Array<number | string>>();
+  });
+
+  it("works with a tuple with mixed array and non array items", () => {
+    const result = flat([1, []] as [number, Array<string>], 1);
+    expectTypeOf(result).toEqualTypeOf<[number, ...Array<string>]>();
+  });
+
+  it("works with a tuple with mixed array and non array items, deeply", () => {
+    const result = flat([[1], []] as [[number], Array<Array<string>>], 2);
+    expectTypeOf(result).toEqualTypeOf<[number, ...Array<string>]>();
+  });
+
+  it("works on non-empty arrays", () => {
+    const result = flat([[1]] as NonEmptyArray<NonEmptyArray<number>>, 1);
+    expectTypeOf(result).toEqualTypeOf<NonEmptyArray<number>>();
+  });
+
+  it("works on tuples inside arrays", () => {
+    const result = flat([] as Array<[Array<string>, Array<number>]>, 2);
+    expectTypeOf(result).toEqualTypeOf<Array<number | string>>();
+  });
+
+  it("works on tuples inside arrays", () => {
+    const result = flat([1, [], 4] as [1, Array<[2, 3]>, 4], 2);
+    expectTypeOf(result).toEqualTypeOf<[1, ...Array<2 | 3>, 4]>();
+  });
+
+  it("works with depths beyond 20", () => {
+    // The built-in type for `Array.prototype.flat` only goes up to 20.
+
+    const result = flat(
+      [[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[1]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]] as const,
+      99,
+    );
+    expectTypeOf(result).toEqualTypeOf<[1]>();
+  });
+
+  it("doesn't accept non-literal depths", () => {
+    // @ts-expect-error [ts2345] - non-literal numbers can't be used as depth.
+    flat([], 1 as number);
+  });
+
+  it("doesn't accept built-in 'infinite' numbers", () => {
+    // They are all typed as `number` by typescript's libs.
+
+    // @ts-expect-error [ts2345] - Infinity is typed as a non-literal number. - https://github.com/microsoft/TypeScript/blob/main/src/lib/es5.d.ts#L9
+    flat([], Infinity);
+
+    // @ts-expect-error [ts2345] - Max number is typed as a non-literal number. - https://github.com/microsoft/TypeScript/blob/main/src/lib/es5.d.ts#L576
+    flat([], Number.MAX_VALUE);
+
+    // @ts-expect-error [ts2345] - Infinity is typed as a non-literal number. - https://github.com/microsoft/TypeScript/blob/main/src/lib/es5.d.ts#L597
+    flat([], Number.POSITIVE_INFINITY);
+  });
+});
+
+// These tests are copied from an previous implementations of the same concept
+// as flat that existed in previous versions of Remeda. We copy the tests so
+// that we can ensure that the new function is equivalent. In the future these
+// can be deleted.
+describe("LEGACY", () => {
+  describe("`flatten` equivalent (depth = 1)", () => {
+    test("flatten", () => {
+      expect(flat([[1, 2], 3, [4, 5]])).toStrictEqual([1, 2, 3, 4, 5]);
+    });
+
+    test("nested", () => {
+      expect(
+        flat([
+          [1, 2],
+          [[3], [4, 5]],
+        ]),
+      ).toStrictEqual([1, 2, [3], [4, 5]]);
+    });
+
+    describe("dataLast", () => {
+      test("flatten multiple values", () => {
+        expect(pipe([[1, 2], 3, [4, 5]], flat())).toStrictEqual([
+          1, 2, 3, 4, 5,
+        ]);
+      });
+
+      test("flatten single value", () => {
+        expect(pipe([[1]], flat())).toStrictEqual([1]);
+      });
+
+      test("lazy", () => {
+        const counter1 = createLazyInvocationCounter();
+        const counter2 = createLazyInvocationCounter();
+        const result = pipe(
+          [[1, 2], 3, [4, 5]],
+          counter1.fn(),
+          flat(4),
+          counter2.fn(),
+          find((x) => x - 1 === 2),
+        );
+        expect(counter1.count).toHaveBeenCalledTimes(2);
+        expect(counter2.count).toHaveBeenCalledTimes(3);
+        expect(result).toStrictEqual(3);
+      });
+    });
+  });
+
+  describe("`flattenDeep` equivalent (depth = 4)", () => {
+    test("flatten", () => {
+      expect(flat([[1, 2], 3, [4, 5]], 4)).toStrictEqual([1, 2, 3, 4, 5]);
+    });
+
+    test("nested", () => {
+      expect(
+        flat(
+          [
+            [1, 2],
+            [[3], [4, 5]],
+          ],
+          4,
+        ),
+      ).toStrictEqual([1, 2, 3, 4, 5]);
+    });
+
+    test("lazy", () => {
+      const counter1 = createLazyInvocationCounter();
+      const counter2 = createLazyInvocationCounter();
+      const result = pipe(
+        [[1, 2], [[3]], [[4, 5]]],
+        counter1.fn(),
+        flat(4),
+        counter2.fn(),
+        find((x) => x - 1 === 2),
+      );
+      expect(counter1.count).toHaveBeenCalledTimes(2);
+      expect(counter2.count).toHaveBeenCalledTimes(3);
+      expect(result).toStrictEqual(3);
+    });
+  });
+});
diff --git a/src/last.test.ts b/src/last.test.ts
index 06a2779..526169f 100644
--- a/src/last.test.ts
+++ b/src/last.test.ts
@@ -40,5 +40,23 @@ describe("last", () => {
       const data = pipe("this is a text", (text) => text.split(""), last());
       assertType<string | undefined>(data);
     });
+
+    test("should return undefined for empty tuples", () => {
+      const input = [] as const;
+      const data = last(input);
+      assertType<undefined>(data);
+    });
+
+    test("can infer last type from const arrays", () => {
+      const input = [3, "a", false] as const;
+      const data = last(input);
+      assertType<false>(data);
+    });
+
+    test("a bit more complex example", () => {
+      const input = [["a", 1] as const, true, { foo: "bar" }] as const;
+      const data = last(input);
+      assertType<{ foo: "bar" }>(data);
+    });
   });
 });