Rejecting array inputs with JS Proxy

602 Views Asked by At

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);

1

There are 1 best solutions below

3
On BEST ANSWER

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');
    }
  }
});