1

I want to create an array that only accepts a certain instance type to be stored. It seems the best solution was to use Proxies, inspired by this gist and SO thread.

So I have a proxy working and for basic array functionality it's working as expected. The set property makes sure that only objects which are an instance of Fruit can be inserted into the Array, or else a TypeError is thrown. the only other property that can be set right now is the length.

The problem is with advanced i/o such as splice(). Logging the set function shows that the array items get shifted to make space for the new item to be inserted at [0], but when the new item is rejected, it leaves the array in a mess.

As set is called iteratively, I don't see a clear-cut way of preventing the splice from initiating, or restoring the array to it's former glory (preferably the former option) within the proxy. Does anyone else know how to implement either of these ideas, or have another suggestion?

"use strict";

class Fruit {
  constructor(name) {
    this._name = name;
  }

  set name(name) {
    this._name = name;
  }

  get name() {
    return this._name;
  }
}

class Vegetable {
  constructor(name) {
    this.name(name);
  }

  set name(name) {
    this._name = name;
  }

  get name() {
    return this._name;
  }
}

// a proxy for our array
var fruitbowl = new Proxy([], {
  apply: function(target, thisArg, argumentsList) {
    return thisArg[target].apply(this, argumentList);
  },
  deleteProperty: function(target, property) {
    console.log("Deleted %s", property);
    return true;
  },
  set: function(target, property, value, receiver) {
    // UNCOMMENT HERE for useful output:
    // console.log("Setting " + property + " to ", value);
    if (property == "length") {
      target.length = value;
      return true;
    } else {
      if (value instanceof Fruit) {
        target[property] = value;
        return true;
      } else {
        return false;
        // throw TypeError("Expected Fruit, got " + typeof(value) + " (" + value + ")");
      }
    }
  }
});

console.log("\n\n=== Putting fruit into the bowl... ===\n\n");

try {
  fruitbowl.push(new Vegetable("potato"));
} catch (e) {
  console.log("Shoudln't allow vegetables: PASSED");
}

fruitbowl.push(new Fruit("apple"));
console.log("Should allow fruit: " + (fruitbowl.length == 1 ? "PASSED" : "FAILED"));

fruitbowl[0] = new Fruit("orange");
console.log("Should replace item specified as long as it's a Fruit: " + (fruitbowl.length == 1 && fruitbowl[0].name == "orange" ? "PASSED" : "FAILED"));

try {
  fruitbowl[0] = "Bananas!!1one";
} catch (e) {

}
console.log("Should not replace with a string: " + (fruitbowl.length == 1 && fruitbowl[0].name == "orange" ? "PASSED" : "FAILED"));

fruitbowl.push(new Fruit("banana"), new Fruit("pear"));
console.log("Should have 3 items [orange, banana, pear]: " + (fruitbowl.length == 3 ? "PASSED" : "FAILED"), fruitbowl);

console.log("\n\n === Cropping the bowl... ===\n\n");

fruitbowl.length = 2;
console.log("Should have 2 items [orange,banana]: " + (fruitbowl.length == 2 ? "PASSED" : "FAILED"));
console.log("Should error at item 2: " + (!fruitbowl[2] ? "PASSED" : "FAILED"), fruitbowl);

console.log("\n\n === Splicing the bowl... ===\n\n");

console.log(fruitbowl.length);

try {
  console.log(fruitbowl.length);
  fruitbowl.splice(0, 0, "pineapples!!1one");
  console.log(fruitbowl.length);
} catch (e) {
  console.log("Shouldn't have inserted string: PASSED");
}
console.log("Should still only have 2 fruit: " + (fruitbowl.length == 2 ? "PASSED" : "FAILED (" + fruitbowl.length + ")"));
console.log(fruitbowl);

4
  • 1
    Arrays don't have a [[Call]] method. Your apply trap is useless. Commented Dec 17, 2016 at 13:47
  • I think you'll be better of with extending the Array prototype redefining all the methods on that new prototype that may mutate the array. Commented Dec 17, 2016 at 14:00
  • @trincot That was my first idea, which worked really well, until it came to overloading the [] shorthand. Commented Dec 17, 2016 at 14:38
  • 1
    I don't think you'd want to change the behaviour of [], as your code (or third party code) might run into unexpected results when wanting to use other, normal arrays. Your new prototype could define its own '.from()` method, and MyArray.from([]) would return an empty array of your prototype. Commented Dec 17, 2016 at 14:43

1 Answer 1

1

As far as I know, the only way to achieve this is to override the splice() function. You have to check if all items are Fruit objects, and if not, throw an error. If they all are Fruit objects, you should call the original function.

Reflect.defineProperty(fruitbowl, 'splice', {
  configurable: true,
  enumerable: false,
  value: function(start, deleteCount, ...items) {
    if (items.every(item => item instanceof Fruit)) {
      return Reflect.apply(Array.prototype.splice, this, [start, deleteCount, ...items]);
    } else {
      throw new Error('All elements must be Fruit objects');
    }
  }
});
Sign up to request clarification or add additional context in comments.

3 Comments

What if someone does [].splice.call(fruitbowl, 0, 1)?
@trincot Then it won't throw an error. I don't think there's any good solution for this, because there's no way to determine which method was called.
I did consider this option, but then started wondering how many other methods would I also have to override.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.