I recently made a mistake I’ve made before. This time decided to never again use get or set in JavaScript. I simplified a class to show the problem. Here’s some code without any issues
class Grid {
#rows=0;
get rows() { return this.#rows; }
set rows(rows) {
if (typeof rows != 'number' || rows < 0) { throw Error();}
this.#rows = rows;
}
}
const grid = new Grid();
grid.rows = 3;
console.log(\`grid has ${grid.rows} rows\`);
Everything works great. I can put checks in the setter to ensure the value it is set to is a positive number. And the getter lets me make the field private so it can’t be changed outside of the class.
Then I decided rowCount would be a better name than rows
class Grid {
#rowCount=0;
get rowCount() { return this.#rowCount; }
set rowCount(count) {
if (typeof count != 'number' || count < 0) { throw Error();}
this.#rowCount = rowCount;
}
}
const grid = new Grid();
grid.rowCount = 3;
console.log(\`grid has ${grid.rowCount} rows\`);
//...
grid.rows = 4;
I forgot to change one of the references from grid.rows to grid.rowCount. So now I have a grid with two properties “#rowCount=3” and “rows=4”. There’s no exception or other obvious indication that I have a problem.
When grid.rows is used, it simply adds a field to the object for the missing setter or returns “undefined” for the getter.
In a single .js file, it’s not too hard to change all references. But in a larger application, there’s no easy way to ensure that every get() or set() call has been changed to rowCount. It’s not only a problem when names change. Misspellings, or forgetting if a field is value or val have similar problems where the code runs “fine” but does not work as expected.
The simple solution is to create getField() and setField() methods instead of using getters and setters
class Grid {
#rows=0;
getRows() { return this.#rows; }
setRows(rows) {
if (typeof rows != 'number' || rows < 0) { throw Error();}
this.#rows = rows;
}
}
If the name changes, then any call to the method with the old name at least throws an exception.
Proxy Alternative
If you really want to use getters and setters rather than getField() and setField(), it can be done with a Proxy. This static class can be used to create a proxy for any other class and does not allow access to fields without using getters and setters or other methods.
class SafeGetSetProxy {
static create(object) {
return new Proxy(object, SafeGetSetProxy.handler);
}
static handler = {
get(target, prop, receiver) {
if (target[prop] !== undefined) {
return target[prop];
}
log.error(`unknown property ${prop}`);
throw Error(`unknown property ${prop}`);
},
set(target, prop, value, receiver) {
if (target[prop] !== undefined) {
return target[prop] = value;
} else {
log.error(`unknown property ${prop}`);
throw Error(`unknown property ${prop}`);
}
}
}
}
Use create() to return a proxy from the constructor of the class you want to have safe getters and setters and not direct field access. We almost always just assume that new returns an instance of the class we specify. But a constructor can return any type of object, not just an instance of the class it is in.
class Test {
#val = null;
constructor(val = null) {
this.#val = val;
return SafeGetSetProxy.create(this);
}
set val(v) { this.#val = v; }
get val() { return this.#val; }
}
When a new Test object is created, a Proxy is returned instead of a Test
const test = new Test();
// test is a Proxy(test) not a Test
Proxy is a standard JavaScript object that lets you replace basic Object operations. In this case we are replacing getters and setters. Everything else works as if you have a normal Test instance. Proxy constructor takes 2 arguments
Proxy(targetObject, handlerObject)
In this case, the targetObject is what “new Test()” would have returned. handlerObject is a static Object in SafeGetSetProxy that implements get() and set(). These methods are called any time a getter or setter would have been called whether or not a getter or setter exist in the target. Calling code cannot accidentally access a field when a getter or setter doesn’t exist because these methods are called instead. If the target object implements the property getter or setter, it works as it should. If the target object does not implement the property, an exception is thrown.
You can see the code in this post working on my github.io page or in the github repository.
The best place to contact me if you have corrections or questions is Twitter.