skip to Main Content

I try to create a "keyed" array, where items can be accessed by the index (like a regular array) and a .key property defined in the item object.

The problem(s):

  • I have concern about the consistency
  • Array methods can be overridden
  • Implement all methods (.slice, .splice. etc.)
class Commands extends Array {
  constructor(items) {
    super(...items);

    items.forEach(item => {
      this[item.key] = item;
    });
  }

  push(...items) {
    super.push(...items);

    items.forEach(item => {
      this[item.key] = item;
    });
  }
}

const commands = new Commands([
  {
    key: "a",
    name: "A",
  },
  {
    key: "b",
    name: "B",
  },
  {
    key: "c",
    name: "C",
  },
]);

console.log("commands", commands);

console.log("item a", commands.a);
console.log("item c", commands[2]);

commands.a.name = "foo";
//commands.slice(0, 1); // throws error "TypeError: Spread syntax requires ...iterable[Symbol.iterator] to be a function"

console.log(commands);

So my question is: Is this a good approach (in terms of "for most use cases"), or should handle it in a other way, e.g. wrap the array in a proxy and handle the "get by key" stuff in get trap, or something complete else?

EDIT:
Answer to @pilchard question in the comments: I want to have a "shortcut", something that is user defined, to access the item. In a large array to work with indexes is not as easy as to say you want item commands.c compared to:

const index = commands.indexOf((item) => {
  return item.key === "c";
});

const element = commands[index];

3

Answers


  1. Your approach to creating a "keyed" array by extending the Array class and adding properties to access items by their keys is interesting and has some merits. However, there are also potential issues and alternative solutions to consider.

    Direct Access by Key: You can access elements directly by their key, which is convenient.
    Array-like Behavior: The class still retains standard array behavior, allowing for methods like push, slice, etc.

    Consistency: The approach may lead to inconsistencies, especially if items are modified or removed. For instance, if an item is removed, its key property will still be accessible unless explicitly deleted.
    Array Methods: Overriding all array methods to maintain the key-indexed behavior can be cumbersome and error-prone. Methods like slice, splice, and others would need to be carefully handled to update the keyed properties correctly.

    Performance: The approach might not be as performant for very large arrays, as updating the keyed properties involves iterating over the items.
    Alternative Approach: Using a Proxy
    A Proxy can provide a cleaner and more flexible way to handle the "get by key" behavior. Here’s an example of how you can achieve this:

    class Commands {
      constructor(items) {
        this.items = items;
        this.proxy = new Proxy(this.items, {
          get: (target, property) => {
            if (property in target) {
              return target[property];
            }
            return target.find(item => item.key === property);
          }
        });
      }
    
      push(...newItems) {
        this.items.push(...newItems);
      }
    
      get proxyArray() {
        return this.proxy;
      }
    }
    
    const commands = new Commands([
      { key: "a", name: "A" },
      { key: "b", name: "B" },
      { key: "c", name: "C" },
    ]);
    
    console.log("commands", commands.proxyArray);
    console.log("item a", commands.proxyArray.a);
    console.log("item c", commands.proxyArray[2]);
    
    commands.proxyArray.a.name = "foo";
    console.log(commands.proxyArray);
    
    // Usage example for accessing by indexOf
    const index = commands.proxyArray.findIndex(item => item.key === "c");
    const element = commands.proxyArray[index];
    console.log("element by key 'c'", element);

    Explanation
    Proxy Handler: The Proxy handler intercepts the get operation. If the property exists as an index, it returns the corresponding item. Otherwise, it searches for the item by its key.
    Simplified Management: You don’t need to override array methods. The Proxy takes care of returning items based on either index or key.
    Flexibility: This approach maintains the flexibility and consistency of accessing elements either by index or key without modifying the original array methods.
    To conclude
    Using a Proxy provides a more robust solution for your use case. It simplifies managing the dual access pattern (by index and key) and avoids the pitfalls of overriding array methods. For most use cases, this method will likely be more maintainable and less error-prone.

    Login or Signup to reply.
  2. You can use a Proxy to retrieve values from an Array two ways. Here’s an example:

    const commands = CommandsFactory([
      { key: "a", name: "A", },
      { key: "b", name: "B", },
      { key: "c", name: "C", },
    ]);
    
    console.log(`commands[2]: ${commands[2]}`);
    console.log(`commands.c: ${commands.c}`);
    console.log(`commands[5]: ${commands[5]}`);
    console.log(`commands.iDontExist: ${commands.iDontExist}`);
    
    
    function CommandsFactory(arr) {
      // the proxy get handler returns the
      // *name* property from either a numeric 
      // value (index) or from a given key.
      // Returns undefined for non existing
      // indexes or keys
      const prxy = {
        get: (target, key) => {
          return key in target && target[key]?.name || 
            target.find(v => v.key === key)?.name;
        } 
      }
    
      return new Proxy(arr, prxy);
    }
    Login or Signup to reply.
  3. You can use Proxy, to provide access with both the keys and indices. You can also cache the found items by key, reset the cache on any methods that mutate array

    class Commands extends Array {
        constructor() {
            super(...arguments);
            return new Proxy(this, {
                get: (...args) => {
                    const prop = args[1];
                    if (prop in this) {
                        return Reflect.get(...args);
                    }
                    return this.find(item => item.key === prop);
                },
    
            });
        }
    }
    
    const commands = new Commands(...[
        {
            key: "a",
            name: "A",
        },
        {
            key: "b",
            name: "B",
        },
        {
            key: "c",
            name: "C",
        },
    ]);
    
    console.log("commands", commands);
    console.log("item a", commands.a);
    console.log("item c", commands[2]);
    
    commands.a.name = "foo";
    
    console.log(commands.slice(0, 1));
    .as-console-wrapper{ max-height: 100% !important}
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search