Creating a setter function in JavaScript Objects
Tags: javascript, objects, getter, setter
Published: 2020-11-17 09:12:01

As part of one of my projects I have had a central object called "layout" with it being to do with manipulating SVGs, and as part of that I decided it would be easier to write said layout object such that it has a getter and setter function, so that:

```javascript
layout.item.points.X = 20;
```

...becomes:

```javascript
layout.set('item.points.X',20);
```

...much more verbose and much more functional, I'm sure you'll agree!

So first, the getter function, well, this couldn't be easier really:

```javascript
const layout = {
  get: function() {
    return this;
  },
  set: // To be confirmed
  /* All other properties */
}
```

But now, what about the setter? Well, that's where it gets a bit more complicated. First, we need to take the propChain and newState from the user:

```javascript
const layout = {
  set: function(propChain,newState) {}
}
```

I have written it so that it follows the usual object notation, as per the example above, so if you want to set item's pointX value to 20 you give it `layout.set('item.points.X',20)`.

First of all, let's prevent the user from being able to change either the setter or getter:

```javascript
const layout = {
  set: function(propChain,newState) {
    if (['get','set'].includes(propChain)) {
      console.warn(`🛑 What are you doing? Should you really be changing either of these functions? `);
      return;
    }
  }
}
```

And get our function to exit if the propChain is empty:

```javascript
const layout = {
  set: function(propChain,newState) {
    if (['get','set'].includes(propChain)) {
      console.warn(`🛑 What are you doing? Should you really be changing either of these functions? `);
      return;
    }
    if (!propChain) return;
  }
}
```

Now for the fun bit! We want to split our propChain into sections and:
 - Check for the first property on our original object, and create it if it isn't there
 - If it is there (which it now is) check if we're at the end of the provided chain
 - If we are, set the property to equal the given newState
 - If not, go one level further into the object (having just created the new property) and repeat from step 1

```javascript
const layout = {
  set: function(propChain,newState) {
    if (['get','set'].includes(propChain)) {
      console.warn(`🛑 What are you doing? Should you really be changing either of these functions? `);
      return;
    }
    if (!propChain) return;
    propChain.split('.').reduce((original/*Object*/,prop,level/*how deep we are into the chain*/) => {},this);
  }
}
```

So, um, what?

Let's go through it bit-by-bit.

We use the split function to split propChain from a string into an array, using . as the breaking point (just how you'd access an object property in JavaScript anyway) on which we can now use the reduce function.

The reduce function is immensely powerful and I'm often guilty of discarding it in favour of map because I'm more comfortable there.

The reduce function takes up to 4 parameters ([read more on MDN](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/reduce)) but we're only interested in the first 3: the accumulated value, the current value and the current index, which we're calling original, prop and level.

```javascript
const layout = {
  set: function(propChain,newState) {
    if (['get','set'].includes(propChain)) {
      console.warn(`🛑 What are you doing? Should you really be changing either of these functions? `);
      return;
    }
    if (!propChain) return;
    propChain.split('.').reduce((original,prop,level) => {
      // Firstly, check if our original object has the property, and add it if not.
      if (!(prop in original)) {
        original[prop] = {}; // Why a blank object? In case we go deeper into the chain and need to add properties to this, which you can't on undefined, 0 or an empty string
      }
    },this);
  }
}
```

Could I not have used `original.hasOwnProperty(prop)`? In JavaScript yes *but* in TypeScript the linter shouts at you: `Do not access Object.prototype method 'hasOwnProperty' from target object  no-prototype-builtins`.

```javascript
const layout = {
  set: function(propChain,newState) {
    if (['get','set'].includes(propChain)) {
      console.warn(`🛑 What are you doing? Should you really be changing either of these functions? `);
      return;
    }
    if (!propChain) return;
    propChain.split('.').reduce((original,prop,level) => {
      if (!(prop in original)) {
        original[prop] = {};
      }
      // Now check if we're at the end of our given chain and if we are, set the property to the given newState
      if (level === propChain.split('.').length - 1 /*Remember, indexing starts at 0*/) {
        original[prop] = newState;
      }
      // Now return our object, and that's it!
      return original[prop];
    },this);
  }
}
```

Finally we arrive at:

```javascript
const layout = {
  get: function() {
    return this;
  },
  set: function(propChain,newState) {
    if (['get','set'].includes(propChain)) {
      console.warn(`🛑 What are you doing? Should you really be changing either of these functions? `);
      return;
    }
    if (!propChain) return;
    propChain.split('.').reduce((original,prop,level) => {
      if (!(prop in original)) {
        original[prop] = {};
      }
      if (level === propChain.split('.').length - 1) {
        original[prop] = newState;
      }
      return original[prop];
    },this);
  }
}
```

Or, in TypeScript:

```typescript
interface LayoutObject extends Record<string, unknown> {
  get: () => LayoutObject;
  set: (
    propChain: string,
    newState: Record<string, unknown> | string | number
  ) => void;
  // All the rest of our properties
}

// TypeScript uses interfaces to define, well, almost everything!

const layout: LayoutObject = {
  get: function (): LayoutObject {
    return this;
  },
  set: function (
    propChain: string,
    newState: Record<string, unknown> | string | number
  ): void {
    if (['get', 'set'].includes(propChain)) {
      console.warn(
        `🛑 What are you doing? Should you really be changing either of these functions?`
      );
      return;
    }
    if (!propChain) return;
    propChain.split('.').reduce((original, prop, level) => {
      if (!(prop in original)) {
        original[prop] = {};
      }
      if (level === propChain.split('.').length - 1) {
        original[prop] = newState;
      }
      return original[prop];
    }, this);
  },
  // All the rest of the properties
}
```