Repository: immutable-js/immutable-js (ts)

Issue:
`updateIn` throw `Uncaught TypeError: keyPath.slice is not a function` for ArrayLike that does not implement `slice` if the keyPath can not update
## Description

The `updateIn` function does call [`coerceKeyPath`](https://github.com/immutable-js/immutable-js/blob/96bf1f9721abf3e18c874400930dd6764d8116b9/src/functional/updateIn.js#L19) on the keyPath.

The coerceKeyPath does check [if the keyPath is an "ArrayLike"](https://github.com/immutable-js/immutable-js/blob/main/src/utils/coerceKeyPath.ts#L5). If true, the value is kept.

In the updateIn, if the keyPath does not match the object, then [we call this line](https://github.com/immutable-js/immutable-js/blob/96bf1f9721abf3e18c874400930dd6764d8116b9/src/functional/updateIn.js#L42-L47), that does construct its error message with `keyPath.slice(0, i)`, but `.slice` might not exist on a custom ArrayLike method.



Here is the implementation of `isArrayLike`: https://github.com/immutable-js/immutable-js/blob/main/src/utils/isArrayLike.ts

## Reproductible example


Define the custom array
```ts
class CustomArrayLike<T> implements ArrayLike<T> {
  readonly length: number;
  readonly [n: number]: T;

  constructor(length: number) {
    this.length = length;
    for (let i = 0; i < length; i++) {
      this[i] = undefined as any;
    }
  }

  // Define other methods if needed, but do not include the `slice` method
  // For example, you can define a method to set values
  set(index: number, value: T): void {
    if (index >= 0 && index < this.length) {
      this[index] = value;
    } else {
      throw new RangeError('Index out of bounds');
    }
  }

  // Define a method to get values
  get(index: number): T {
    if (index >= 0 && index < this.length) {
      return this[index];
    } else {
      throw new RangeError('Index out of bounds');
    }
  }
}
```

Code that throws
```ts
// create an ArrayLike
const customArray = new CustomArrayLike<number>(2);
customArray.set(0, 10);
customArray.set(1, 20);

// code that works perfectly
Immutable.updateIn({ 10 :{ 20 : 'a' }}, customArray2, (v) => `${v.toUpperCase()}`)
// does output { 10 :{ 20 : 'A' }}


Immutable.updateIn({ 10 : 'a' }, customArray2, (v) => `${v.toUpperCase()}`)
// does throw Uncaught TypeError: keyPath.slice is not a function
```

Pull request description:
Fix `Uncaught TypeError: keyPath.slice is not a function` for ArrayLike method

Fixes #2062

Patch:
diff --git a/src/functional/updateIn.js b/src/functional/updateIn.js
index bf27c8b81..7567b58f5 100644
--- a/src/functional/updateIn.js
+++ b/src/functional/updateIn.js
@@ -41,7 +41,7 @@ function updateInDeeply(
   if (!wasNotSet && !isDataStructure(existing)) {
     throw new TypeError(
       'Cannot update within non-data-structure value in path [' +
-        keyPath.slice(0, i).map(quoteString) +
+        Array.from(keyPath).slice(0, i).map(quoteString) +
         ']: ' +
         existing
     );


Test patch:
diff --git a/__tests__/updateIn.ts b/__tests__/updateIn.ts
index 8a8540e52..1d4fd5c91 100644
--- a/__tests__/updateIn.ts
+++ b/__tests__/updateIn.ts
@@ -72,6 +72,54 @@ describe('updateIn', () => {
     );
   });
 
+  it('handle ArrayLike objects that are nor Array not immutable Collection', () => {
+    class CustomArrayLike<T> implements ArrayLike<T> {
+      readonly length: number;
+      [n: number]: T;
+
+      constructor(...values: Array<T>) {
+        this.length = values.length;
+
+        for (let i = 0; i < values.length; i++) {
+          this[i] = values[i];
+        }
+      }
+
+      // Define other methods if needed, but do not include the `slice` method
+      // For example, you can define a method to set values
+      set(index: number, value: T): void {
+        if (index < 0 || index >= this.length) {
+          throw new RangeError('Index out of bounds');
+        }
+
+        this[index] = value;
+      }
+
+      // Define a method to get values
+      get(index: number): T {
+        if (index < 0 || index >= this.length) {
+          throw new RangeError('Index out of bounds');
+        }
+
+        return this[index];
+      }
+    }
+
+    // create an ArrayLike
+    const customArray = new CustomArrayLike<number>(10, 20);
+
+    // code that works perfectly
+    expect(
+      // @ts-expect-error -- `updateIn` keypath type should be `OrderedCollection<K> | ArrayLike<K>;
+      updateIn({ 10: { 20: 'a' } }, customArray, (v) => `${v.toUpperCase()}`)
+    ).toEqual({ 10: { 20: 'A' } });
+
+    expect(() =>
+      // @ts-expect-error -- `updateIn` keypath type should be `OrderedCollection<K> | ArrayLike<K>;
+      updateIn({ 10: 'a' }, customArray, (v) => `${v.toUpperCase()}`)
+    ).toThrow('Cannot update within non-data-structure value in path [10]: a');
+  });
+
   it('identity with notSetValue is still identity', () => {
     const m = Map({ a: { b: { c: 10 } } });
     expect(m.updateIn(['x'], 100, (id) => id)).toEqual(m);
@@ -159,7 +207,9 @@ describe('updateIn', () => {
     const m = fromJS({ a: { b: { c: 10 } } });
     expect(() => {
       m.updateIn(['a', 'b', 'c', 'd'], () => 20).toJS();
-    }).toThrow();
+    }).toThrow(
+      'Cannot update within non-data-structure value in path ["a","b","c"]: 10'
+    );
   });
 
   it('update with notSetValue when non-existing key', () => {