Making Ember Objects More Strict: Only Access Defined Properties

Ember.Object provides a flexible starting point for creating models in a single-page web application. It can hold simple data members, define computed properties that automatically update when dependencies change, run a callback when properties change, and extend parent “classes” to create new types with additional members.

That said, I think Ember can be a bit too flexible at times.

How Much Flexibility is Too Much?

Let’s say I create a model for a blog post using Ember Data:


export default DS.Model.extend({
  author: DS.hasOne('author'),
  text: DS.attr('string'),
  likes: DS.attr('number'),
  comments: DS.hasMany('comment')
});

Then in code later on, I can do the following:


const post = this.store.create('blog-post', { 
  author: jim, likes: 5, comments: [] 
});
post.set('text', "Lorem ipsum dolor sit amet...");
post.save();

That’s all well and good. But what if later on, I want to set the body of the post, forget I called it ‘text,’ and think I called it ‘body’?


post2.set('body', "Ain't goin' nowhere");
// a new property 'body' was created and has the set value
post.save();

My tests (automated and manual) will probably fail because the post content is not being updated properly, but it may not be immediately clear what the problem is. This issue is further exacerbated by many-word property names:


const data = this.get('reallyLongPropertyNameEtc1');

or when you are dot-chaining in property gets or sets:


const data = this.get('foo.bar.baz');

If data is undefined, which part is to blame: foo or bar or baz? Again, we can dig in and figure out the answer, but it would be nice if we didn’t have to.

Best Practices

I consider it a best practice to define any property I intend to use at the top of my Ember.Object. If it will be a simple data type, I use something like:


export default Ember.Object.extend({
  aNumber: 0,
  aString: "",
  anArray: Ember.computed(() => []),

  // computed properties...
});

Consequently, I almost never want to get or set a property on an Ember.Object that I did not explicitly define. Ideally, Ember would tell me immediately if I tried to get or set an unknown property and I didn’t intend to do so.

Strict Models

Luckily, it isn’t hard to accomplish this. Ember.Object provides two functions, unknownProperty() and setUnknownProperty(), which can be used to create “strict” models. The presumed intention of these functions is to allow dynamic execution based on the string value of the property name to be retrieved or set.

However, we can repurpose them to make our Ember.Object models more strict. We can implement unknownProperty() and setUnknownProperty() to throw errors providing details of the property meant to be used, which allows us to fail fast and get as close as Ember allows to a locked down, static class.


// as a mixin - this could also be defined on a base class
export default Ember.Mixin.create({
  unknownProperty(key) {
    throw new Error(`attempting to get unknown property ${key}`);
  },
  setUnknownProperty(key,value) {
    throw new Error(`attempting to set unknown property '${key}' to '${value}'`);
  }
});

Now, our first example throws an error when you try to set the wrong property:


post2.set('body', "Ain't goin' nowhere"); // throws error -
// "attempting to set unknown property 'body' to 'Ain't goin' nowhere'"
post.save();

The Exception that Complicates the Rule

Actually, it isn’t quite so simple. On some occasions, the Ember framework (and potentially plugins used by your application) may invoke properties you don’t care about or want to declare. Accordingly, you’ll have to allow some property names even if they are not explicitly defined. For these exceptions, you’ll want to use the default behavior (return undefined for get(), and call defineProperty() for set()). This is frustrating, but I don’t think it reduces the value of strict models by an unreasonable amount.


const exceptions = [
  'isTruthy', // Ember rendering
  'size', 'length', // Ember.isPresent
];
// as a mixin - this could also be defined on a base class
export default Ember.Mixin.create({
  unknownProperty(key) {
    if (!exceptions.contains(key)) {
     throw new Error(`attempting to get unknown property ${key}`);
    }
  },
  setUnknownProperty(key,value) {
    if (!exceptions.contains(key)) {
      throw new Error(`attempting to set unknown property ${key} to ${value}`);
    } else {
      // I'd rather call _super() here, but Ember only auto-defines a property
      // when there is no implementation of setUnknownProperty(), as opposed to
      // making defineProperty the default implementation
      Ember.defineProperty(this, key, null, value);
      return value;
    }
  }
});

Types, But Not Too Verbose

On my current project, making all of our data transfer objects into strict models has already helped me catch and fix errors more quickly than I would have otherwise. In general, I’d rather have my code throw an error than fail implicitly or silently. And as I wrote about with generating Ember models from C#, even if JavaScript is not statically typed, I’ll nudge it in that direction when I can do so–as long as it doesn’t make my code overly verbose.

Conversation
  • Luke says:

    Thanks for the article. This could be a useful technique.

    Minor point: in the section titled Best practices you give a code example where you assign an array literal to a property. In general this is not JavaScript best practice as instances will share that property which is almost always not the desired behaviour.

    Cheers

    • Will Pleasant-Ryan Will Pleasant-Ryan says:

      Good point. I should know better – I’ve run into that issue before more than once, but it slipped my mind when writing up the example. I’ve updated the post.

  • Comments are closed.