nick comer

programmer, tinkerer, learner

Interfaces in Vanilla ES6 JavaScript

Recently, I found myself implementing a library in a few different languages. I started in PHP, which is pretty much Java’s weird multi-paradigm little-brother. Thus, it has compile-time interfaces that will ensure that classes adhere to and implement specific functionality.

Next up, was Go. Go has an exceptional perspective on interfaces that gets rid of the need to explicitly say that a given struct actually implements an interface. So with Go, we were good to… go. (I am not sorry).

Next up, was JavaScript. My library I was implementing was heavily dependent on OOP concepts and leaned on interfaces heavily. It needs for objects in the language to always be ready to describe themselves in certain ways. For the sake of example, let’s say there is an interface that I need to emulate called ThingInterface. It needs to demand that an object can give us its type, and it needs to be able to be asked for a specific attribute and return a value that attribute.

Looking around at the new ES6 features for options to solve this problem, I landed on Symbols. Symbols have the property of being very unique. For example:

Symbol("foo") === Symbol("foo"); // false

This can give guarantees that we will not collide with another library who has similarly named functionality.

Another property that makes them nice, is that they maintain somewhat of a global registry for them in the runtime using Symbol.for.

// a.js
import { sym } from "./b";
b.sym === Symbol.for("b"); // true

// b.js
export const sym = Symbol.for("b");

This .for function gives you the same benefit that Go has where you do not have to explicitly import a library just to say it implements something. With symbols, you can pull these symbols out of nowhere and just define without having to worry about the library that will be using them. You can also avoid creating circular dependencies.

Using Symbols, you can create objects that make functionality specifically for libraries. In the case of my library, the implementation would check each object passed to it for certain Symbols and assert their presence or throw an error.

// thing-interface.js
const has = (obj, k) => Object.prototype.hasOwnProperty(obj, k);

const thingType = Symbol.for("thing-interface.thing-type");
const getThingAttribute = Symbol.for("thing-interface.get-thing-attr");

function isThing(thing) {
  return (
    typeof thing === "object" &&
    has(thing, thingType) &&
    has(thing, getThingAttribute)
  );
}
function assertIsThing(thing) {
  if (!isThing(thing)) {
    throw new TypeError("Not a Thing!");
  }
}
export function describeThing(thing) {
  assertIsThing(thing);
  console.log(
    "type: %s, id: %s",
    thing[thingType],
    thing[getThingAttribute]("id")
  );
}

// user.js
import { describeThing } from "./thing-interface";
const priv = Symbol("user.priv");
class User {
  constructor(data) {
    this[priv] = { data };
    this[Symbol.for("thing-interface.thing-type")] = "user";
  }

  [Symbol.for("thing-interface.get-thing-attr")](k) {
    return this[priv].data[k] || null;
  }
}

var john = new User({ id: "123" });
describeThing(john); // type: user, id: 123
describeThing({
  type: "user",
  attrs: {
    id: "123",
    status: "active",
  },
}); // Uncaught TypeError: Not a Thing!

With this pattern, libraries can require functionality be in place and have clean contracts with other components in an application without having to rely on heuristics.

The only drawbacks here are having to constantly assert that an object conforms to the interface. Because objects can change whenever, these assertions must be made each time they are passed to a library. If these calls are in a hot path, it would be worth making sure the assertions are fast.