A little while ago my colleagues and I came across a fairly interesting problem that we couldn’t immediately solve. We use the Immutable library quite extensively — especially records since they have the nicety of allowing for dot accessors and object destructuring.

So let’s say we have a record of the following shape:

const record = Record({

a: 1,

b: 2,

c: 3

})();

We wanted to pull out one of the properties off this record and organize the rest in a separate object, so we destructured as we normally do:

const { a, ...rest } = record;

Do you see the problem with what we did? We didn’t. And it wasn’t until a lot of digging that we were able to come up with why rest was always undefined .

The answer, we discovered, was that while Immutable records allow for dot accessing and destructuring, none of the properties on a record are enumerable, and guess what kind of properties the ... object rest operator groups together? That’s right, enumerable properties. And so we will never get an object that isn’t undefined using this method.

But throughout the debugging process, I went down a pretty big rabbit hole surrounding the ways you can configure an object’s properties and customize its functionality using vanilla JS. With this knowledge we can recreate much of the behavior of Immutable records or even vastly increase the basic functionality of our objects’ default behavior.

To work with a concrete example we’ll create a possible database entry that includes calculated properties, pre- and post-processing on accessing and setting properties, and some meta-programmatic features to give our entry some self-awareness of its execution context.

Getting Started

To begin we’ll scope out the features of our entry. It would be nice to implement a(n):

Id

Name

Last accessed date

Last modified date

Birthdate

SSN

So let’s create our database entry:

const dbEntry = Object.create( null );

I chose to use Object.create instead of the more familiar {} syntax because the former entry allows us to specify what the object’s prototype should be, and by passing in null , I am de-linking this object from any prototype. In effect this object will have absolute no functionality other than what we define on it.

Property Data Descriptors

A property data descriptor is an object assigned to an object’s property (one descriptor per property) that dictates how the JavaScript engine will behave regarding that property. The four keys a data descriptor can have are:

value : the actual value we want the property to be (defaults undefined )

: the actual value we want the property to be (defaults ) enumerable : whether the property should show up in operations that enumerate over an object’s keys, such as for...in loops or Object.keys() (defaults false )

: whether the property should show up in operations that enumerate over an object’s keys, such as loops or (defaults ) configurable : indicates if we can later change the descriptor settings or be able to delete the property off the object (defaults false )

: indicates if we can later change the descriptor settings or be able to delete the property off the object (defaults ) writable : tells if the value of the property can be changed (defaults false )

Now, when we normally set an object’s property, these values are automatically established in a way that gives us complete control over that property.

const obj = {};

obj.a = 1; Object.getOwnPropertyDescriptor(obj, 'a');

// { value: 1,

// writable: true,

// enumerable: true,

// configurable: true }

So how do we go about restricting what can be done when we set a property? Use Object.defineProperty and pass in the configuration we want.

So let’s set an ID on our dbEntry object. Since this represents the primary key it would have in a database, once it’s set we should not be able to change it, but we would probably like to be able to access it if we loop across our entry’s keys. So we’ll do the following:

Object.defineProperty(

dbEntry, // Object we're defining property on

'id', // Property name

{ // Property descriptor

value: 1,

enumerable: true

}

);

We are setting the value of dbEntry.id to 1 (it would be, of course, whatever its actual primary key is) and specifying we want to be able to enumerate it. We didn’t need to pass in the configurable or writable flags because when we define a property and don’t specify a descriptor setting JavaScript will use the respective defaults specified above ( false in these cases). From now on, I’ll specify all descriptor options, even when the defaults are what I want.

Let’s also add properties for SSN and birthdate:

Object.defineProperties(

dbEntry, // Object to define props on

{ // Object of props and descriptors

ssn: {

value: '123-45-6789',

writable: false,

enumerable: true,

configurable: false

},

birthdate: {

value: '01/21/1980',

writable: false,

enumerable: true,

configurable: false

}

}

);

This time we used the Object.defineProperties method to add multiple properties at the same time rather than having to call Object.defineProperty twice. The second parameter is merely an object whose keys will become the properties on the designated object and whose values are the descriptors.

Property Accessor Descriptors

While we can already exercise some nice control over an object by using data descriptors (e.g., enforce immutability, determine how ‘accessible’ a key is), we still have quite a bit of additional power we can exert if we move away from data descriptors (which are fairly static) to accessor descriptors (which allow us to have fairly dynamic object properties.

The main difference between an accessor descriptor and a data descriptor is that accessors replace the earlier value and writable configuration flags with get and set functions.