Repository: remeda/remeda
Pull request #536 (2024-02-28 15:01:41)

fix: remove more `any` from the codebase
Continuing to clean up and strictify the codebase by removing any `any`, using eslint to guide me.

The main change here (and the reason i'm adding a "fix" type so that we bump a version) is the changes to purry/pipe, including changing the compilation flag to allow iterating in es5

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

Latest deployment of this branch, based on commit 3c86dc81656d1e525725b44b5787997cd989052d:

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


Patch:
diff --git a/eslint.config.js b/eslint.config.js
index 809557c..5a3832d 100644
--- a/eslint.config.js
+++ b/eslint.config.js
@@ -3,7 +3,7 @@ const tseslint = require('typescript-eslint');
 
 module.exports = tseslint.config(
   {
-    ignores: ['dist', 'docs'],
+    ignores: ['dist', 'docs', 'examples'],
   },
   js.configs.recommended,
   ...tseslint.configs.strictTypeChecked,
@@ -33,7 +33,6 @@ module.exports = tseslint.config(
 
       // TODO: A lot of related typing issues arise from the fact that `any` is
       // used throughout the codebase.
-      '@typescript-eslint/no-unsafe-assignment': 'off',
       '@typescript-eslint/no-unsafe-call': 'off',
       '@typescript-eslint/no-unsafe-member-access': 'off',
       '@typescript-eslint/no-unsafe-return': 'off',
diff --git a/src/_purryOrderRules.ts b/src/_purryOrderRules.ts
index aab7091..096b272 100644
--- a/src/_purryOrderRules.ts
+++ b/src/_purryOrderRules.ts
@@ -100,7 +100,9 @@ export function purryOrderRulesWithArgument(
   ) => unknown,
   inputArgs: IArguments
 ): unknown {
-  const [first, second, ...rest] = Array.from(inputArgs);
+  const [first, second, ...rest] = Array.from(
+    inputArgs
+  ) as ReadonlyArray<unknown>;
 
   // We need to pull the `n` argument out to make it work with purryOrderRules.
   let arg: unknown;
@@ -162,10 +164,12 @@ function isOrderRule<T>(x: unknown): x is OrderRule<T> {
     return false;
   }
 
-  const [maybeProjection, maybeDirection, ...rest] = x;
+  const [maybeProjection, maybeDirection, ...rest] =
+    x as ReadonlyArray<unknown>;
 
   return (
     isProjection(maybeProjection) &&
+    typeof maybeDirection === 'string' &&
     maybeDirection in COMPARATORS &&
     // Has to be a 2-tuple
     rest.length === 0
diff --git a/src/clone.ts b/src/clone.ts
index 55bac42..8748721 100644
--- a/src/clone.ts
+++ b/src/clone.ts
@@ -1,4 +1,4 @@
-/* eslint-disable @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument -- FIXME! */
+/* eslint-disable @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-unsafe-assignment -- FIXME! */
 
 // from https://github.com/ramda/ramda/blob/master/source/internal/_clone.js
 
diff --git a/src/conditional.ts b/src/conditional.ts
index 24ef6cb..a3b45f8 100644
--- a/src/conditional.ts
+++ b/src/conditional.ts
@@ -208,7 +208,7 @@ function isCase(maybeCase: unknown): maybeCase is Case<unknown, unknown> {
     return false;
   }
 
-  const [when, then, ...rest] = maybeCase;
+  const [when, then, ...rest] = maybeCase as ReadonlyArray<unknown>;
   return (
     typeof when === 'function' &&
     when.length <= 1 &&
diff --git a/src/debounce.ts b/src/debounce.ts
index 962c5ef..09529ac 100644
--- a/src/debounce.ts
+++ b/src/debounce.ts
@@ -1,4 +1,4 @@
-/* eslint-disable @typescript-eslint/no-explicit-any --
+/* eslint-disable @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-assignment --
  * Function inference doesn't work when `unknown` is used as the parameters
  * generic type, it **has** to be `any`.
  */
diff --git a/src/flattenDeep.ts b/src/flattenDeep.ts
index fcfa8ab..bf55045 100644
--- a/src/flattenDeep.ts
+++ b/src/flattenDeep.ts
@@ -1,4 +1,4 @@
-/* eslint-disable @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument -- FIXME! */
+/* eslint-disable @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-unsafe-assignment -- FIXME! */
 
 import { _reduceLazy, LazyResult } from './_reduceLazy';
 import { purry } from './purry';
diff --git a/src/isPlainObject.ts b/src/isPlainObject.ts
index 122369f..33a6fbf 100644
--- a/src/isPlainObject.ts
+++ b/src/isPlainObject.ts
@@ -32,6 +32,7 @@ export function isPlainObject<T>(
     return false;
   }
 
+  // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment -- This is a low-level check, we can't avoid it being typed as `any`.
   const proto = Object.getPrototypeOf(data);
   return proto === null || proto === Object.prototype;
 }
diff --git a/src/pathOr.ts b/src/pathOr.ts
index 19b45c1..f7dc477 100644
--- a/src/pathOr.ts
+++ b/src/pathOr.ts
@@ -1,5 +1,3 @@
-/* eslint-disable @typescript-eslint/no-explicit-any */
-
 import { purry } from './purry';
 
 /**
@@ -148,13 +146,18 @@ export function pathOr() {
   return purry(_pathOr, arguments);
 }
 
-function _pathOr(object: any, path: Array<any>, defaultValue: any): any {
-  let current = object;
+function _pathOr(
+  data: unknown,
+  path: ReadonlyArray<PropertyKey>,
+  defaultValue: unknown
+): unknown {
+  let current = data;
   for (const prop of path) {
-    if (current == null || current[prop] == null) {
-      return defaultValue;
+    if (current == null) {
+      break;
     }
-    current = current[prop];
+    current = (current as Record<PropertyKey, unknown>)[prop];
   }
-  return current;
+
+  return current ?? defaultValue;
 }
diff --git a/src/pipe.ts b/src/pipe.ts
index 7b9d118..61324bc 100644
--- a/src/pipe.ts
+++ b/src/pipe.ts
@@ -195,54 +195,68 @@ export function pipe<A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P>(
 ): P;
 
 export function pipe(
-  value: any,
+  input: unknown,
   ...operations: ReadonlyArray<((value: any) => unknown) | LazyOp>
 ): any {
-  let ret = value;
+  let output = input;
 
-  const lazyOps = operations.map(op =>
+  const lazyOperations = operations.map(op =>
     'lazy' in op ? toPipedLazy(op) : undefined
   );
 
-  let opIdx = 0;
-  while (opIdx < operations.length) {
-    const op = operations[opIdx]!;
-    const lazyOp = lazyOps[opIdx];
-    if (!lazyOp) {
-      ret = op(ret);
-      opIdx++;
+  let operationIndex = 0;
+  while (operationIndex < operations.length) {
+    const lazyOperation = lazyOperations[operationIndex];
+    if (lazyOperation === undefined || !isIterable(output)) {
+      const operation = operations[operationIndex]!;
+      output = operation(output);
+      operationIndex++;
       continue;
     }
 
-    const lazySeq: Array<ReturnType<typeof toPipedLazy>> = [];
-    for (let j = opIdx; j < operations.length; j++) {
-      const lazyOp = lazyOps[j];
+    const lazySequence: Array<ReturnType<typeof toPipedLazy>> = [];
+    for (let j = operationIndex; j < operations.length; j++) {
+      const lazyOp = lazyOperations[j];
       if (lazyOp === undefined) {
         break;
       }
 
-      lazySeq.push(lazyOp);
+      lazySequence.push(lazyOp);
       if (lazyOp.isSingle) {
         break;
       }
     }
 
-    const acc: Array<any> = [];
+    const accumulator: Array<unknown> = [];
 
-    for (const item of ret) {
-      if (_processItem({ item, acc, lazySeq })) {
+    const iterator = output[Symbol.iterator]();
+
+    // eslint-disable-next-line no-constant-condition -- TODO: Once we bump the TS target version above ES5 we can use the built-in for-of loop instead.
+    while (true) {
+      const result = iterator.next();
+      if (result.done ?? false) {
+        break;
+      }
+
+      const shouldExitEarly = _processItem(
+        result.value,
+        accumulator,
+        lazySequence
+      );
+      if (shouldExitEarly) {
         break;
       }
     }
-    const { isSingle } = lazySeq[lazySeq.length - 1]!;
+
+    const { isSingle } = lazySequence[lazySequence.length - 1]!;
     if (isSingle) {
-      ret = acc[0];
+      output = accumulator[0];
     } else {
-      ret = acc;
+      output = accumulator;
     }
-    opIdx += lazySeq.length;
+    operationIndex += lazySequence.length;
   }
-  return ret;
+  return output;
 }
 
 type LazyFn = (value: any, index?: number, items?: any) => LazyResult<any>;
@@ -255,23 +269,19 @@ type LazyOp = ((input: any) => any) & {
   lazyArgs?: ReadonlyArray<unknown>;
 };
 
-function _processItem({
-  item,
-  lazySeq,
-  acc,
-}: {
-  item: any;
-  lazySeq: ReadonlyArray<ReturnType<typeof toPipedLazy>>;
-  acc: Array<any>;
-}): boolean {
-  if (lazySeq.length === 0) {
-    acc.push(item);
+function _processItem(
+  item: unknown,
+  accumulator: Array<unknown>,
+  lazySequence: ReadonlyArray<ReturnType<typeof toPipedLazy>>
+): boolean {
+  if (lazySequence.length === 0) {
+    accumulator.push(item);
     return false;
   }
   let lazyResult: LazyResult<any> = { done: false, hasNext: false };
   let isDone = false;
-  for (let i = 0; i < lazySeq.length; i++) {
-    const lazyFn = lazySeq[i]!;
+  for (let i = 0; i < lazySequence.length; i++) {
+    const lazyFn = lazySequence[i]!;
     const { isIndexed, index, items } = lazyFn;
     items.push(item);
     lazyResult = isIndexed ? lazyFn(item, index, items) : lazyFn(item);
@@ -280,11 +290,11 @@ function _processItem({
       if (lazyResult.hasMany) {
         const nextValues: Array<any> = lazyResult.next;
         for (const subItem of nextValues) {
-          const subResult = _processItem({
-            item: subItem,
-            acc,
-            lazySeq: lazySeq.slice(i + 1),
-          });
+          const subResult = _processItem(
+            subItem,
+            accumulator,
+            lazySequence.slice(i + 1)
+          );
           if (subResult) {
             return true;
           }
@@ -304,7 +314,7 @@ function _processItem({
     }
   }
   if (lazyResult.hasNext) {
-    acc.push(item);
+    accumulator.push(item);
   }
   if (isDone) {
     return true;
@@ -322,3 +332,13 @@ function toPipedLazy(op: LazyOp) {
     items: [] as Array<unknown>,
   });
 }
+
+function isIterable(something: unknown): something is Iterable<unknown> {
+  // Check for null and undefined to avoid errors when accessing Symbol.iterator
+  return (
+    typeof something === 'string' ||
+    (typeof something === 'object' &&
+      something !== null &&
+      Symbol.iterator in something)
+  );
+}
diff --git a/src/purry.ts b/src/purry.ts
index 328f935..4da07ca 100644
--- a/src/purry.ts
+++ b/src/purry.ts
@@ -1,5 +1,12 @@
 /* eslint-disable @typescript-eslint/no-explicit-any */
 
+type LazyFactory = (...args: any) => unknown;
+
+type MaybeLazyFunction = {
+  (...args: any): unknown;
+  lazy?: LazyFactory;
+};
+
 /**
  * Creates a function with `data-first` and `data-last` signatures.
  *
@@ -31,22 +38,25 @@
  * @category Function
  */
 export function purry(
-  fn: any,
-  args: IArguments | ReadonlyArray<any>,
-  lazy?: any
-) {
+  fn: MaybeLazyFunction,
+  args: IArguments | ReadonlyArray<unknown>,
+  lazyFactory?: LazyFactory
+): 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;
-  const arrayArgs = Array.from(args);
   if (diff === 0) {
-    return fn(...arrayArgs);
+    return fn(...callArgs);
   }
+
   if (diff === 1) {
-    const ret: any = (data: any) => fn(data, ...arrayArgs);
-    if (lazy || fn.lazy) {
-      ret.lazy = lazy || fn.lazy;
-      ret.lazyArgs = args;
-    }
-    return ret;
+    const ret = (data: unknown) => fn(data, ...callArgs);
+    const lazy = lazyFactory ?? fn.lazy;
+    return lazy === undefined
+      ? ret
+      : Object.assign(ret, { lazy, lazyArgs: args });
   }
+
   throw new Error('Wrong number of arguments');
 }
diff --git a/src/setPath.ts b/src/setPath.ts
index 914c006..7d76166 100644
--- a/src/setPath.ts
+++ b/src/setPath.ts
@@ -1,4 +1,4 @@
-/* eslint-disable @typescript-eslint/no-explicit-any */
+/* eslint-disable @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-assignment -- FIXME! */
 
 import { purry } from './purry';
 import { Path, SupportsValueAtPath, ValueAtPath } from './_paths';


Test patch:
diff --git a/src/clone.test.ts b/src/clone.test.ts
index bc4cade..ae5cd71 100644
--- a/src/clone.test.ts
+++ b/src/clone.test.ts
@@ -1,4 +1,4 @@
-/* eslint-disable @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument -- FIXME! */
+/* eslint-disable @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-unsafe-assignment -- FIXME! */
 
 import { clone } from './clone';
 
diff --git a/src/forEachObj.test.ts b/src/forEachObj.test.ts
index d8679c1..b9aab24 100644
--- a/src/forEachObj.test.ts
+++ b/src/forEachObj.test.ts
@@ -36,7 +36,7 @@ describe('data_last', () => {
   });
 
   it('forEachObj.indexed', () => {
-    const cb = vi.fn();
+    const cb = vi.fn<[number, string, typeof obj]>();
     const result = pipe(obj, forEachObj.indexed(cb));
     expect(cb.mock.calls).toEqual([
       [1, 'a', obj],
diff --git a/src/isArray.test.ts b/src/isArray.test.ts
index f6cd42a..48b7c79 100644
--- a/src/isArray.test.ts
+++ b/src/isArray.test.ts
@@ -7,7 +7,7 @@ import { isArray } from './isArray';
 
 describe('isArray', () => {
   it('should infer ReadonlyArray<unknown> when given any', () => {
-    // eslint-disable-next-line @typescript-eslint/no-explicit-any -- explicitly testing `any`
+    // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-assignment -- Explicitly testing `any`
     const data = [] as any;
     if (isArray(data)) {
       expectTypeOf(data).not.toBeAny();
diff --git a/src/isBoolean.test.ts b/src/isBoolean.test.ts
index 14f6096..3ad349c 100644
--- a/src/isBoolean.test.ts
+++ b/src/isBoolean.test.ts
@@ -23,7 +23,7 @@ describe('isBoolean', () => {
   });
 
   it('should narrow `any`', () => {
-    // eslint-disable-next-line @typescript-eslint/no-explicit-any -- We are explicitly testing the `any` case here
+    // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-assignment -- Explicitly testing `any`
     const data = TYPES_DATA_PROVIDER.boolean as any;
     if (isBoolean(data)) {
       expect(typeof data).toEqual('boolean');
diff --git a/src/isObjectType.test.ts b/src/isObjectType.test.ts
index 836e201..a22c14c 100644
--- a/src/isObjectType.test.ts
+++ b/src/isObjectType.test.ts
@@ -122,7 +122,7 @@ describe('typing', () => {
   });
 
   test('Can narrow down `any`', () => {
-    // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Explicitly testing `any`
+    // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-assignment -- Explicitly testing `any`
     const data = { hello: 'world' } as any;
     if (isObjectType(data)) {
       expectTypeOf(data).toEqualTypeOf<object>();
diff --git a/src/isPlainObject.test.ts b/src/isPlainObject.test.ts
index 1c87214..4e18632 100644
--- a/src/isPlainObject.test.ts
+++ b/src/isPlainObject.test.ts
@@ -78,7 +78,7 @@ describe('typing', () => {
   });
 
   test('Can narrow down `any`', () => {
-    // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Explicitly testing `any`
+    // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-assignment -- Explicitly testing `any`
     const data = { hello: 'world' } as any;
     if (isPlainObject(data)) {
       expectTypeOf(data).toEqualTypeOf<Record<PropertyKey, unknown>>();
diff --git a/src/isSymbol.test.ts b/src/isSymbol.test.ts
index f171d25..ea7de39 100644
--- a/src/isSymbol.test.ts
+++ b/src/isSymbol.test.ts
@@ -23,7 +23,7 @@ describe('isSymbol', () => {
   });
 
   it('should work even if data type is `any`', () => {
-    // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Explicitly checking any
+    // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-assignment -- Explicitly checking any
     const data = TYPES_DATA_PROVIDER.symbol as any;
     if (isSymbol(data)) {
       expect(typeof data).toEqual('symbol');
diff --git a/src/pick.test.ts b/src/pick.test.ts
index aa9517d..1d621b7 100644
--- a/src/pick.test.ts
+++ b/src/pick.test.ts
@@ -15,9 +15,9 @@ describe('data first', () => {
     }
     class TestClass extends BaseClass {}
     const testClass = new TestClass();
-    expect(pick(testClass, ['testProp'])).toEqual({
-      testProp: expect.any(Function),
-    });
+    expectTypeOf(pick(testClass, ['testProp'])).toEqualTypeOf<{
+      testProp: () => string;
+    }>();
   });
 });
 
diff --git a/src/purry.test.ts b/src/purry.test.ts
index 2042c89..19d25b8 100644
--- a/src/purry.test.ts
+++ b/src/purry.test.ts
@@ -5,21 +5,22 @@ function sub(a: number, b: number) {
 }
 
 test('all arguments', () => {
-  function fn(...args: Array<unknown>) {
+  function fn(...args: ReadonlyArray<unknown>) {
     return purry(sub, args);
   }
   expect(fn(10, 5)).toEqual(5);
 });
 
 test('1 missing', () => {
-  function fn(...args: Array<unknown>) {
+  function fn(...args: ReadonlyArray<unknown>) {
     return purry(sub, args);
   }
-  expect(fn(5)(10)).toEqual(5);
+  const purried = fn(5) as (...args: ReadonlyArray<unknown>) => unknown;
+  expect(purried(10)).toEqual(5);
 });
 
 test('wrong number of arguments', () => {
-  function fn(...args: Array<unknown>) {
+  function fn(...args: ReadonlyArray<unknown>) {
     return purry(sub, args);
   }
   expect(() => fn(5, 10, 40)).toThrowError('Wrong number of arguments');