Implementing Interfaces in JavaScript

There are different ways to extend objects in JS: copying properties, using inheritance, adding mixins, and yet there’s no native way to implement interfaces.

What is an interface?

The following is an extract from my recent book:

An interface is usually a behavior described through methods or properties.

Interfaces can be used to simply describe expectations: they don’t need to implement any logic at all.

As an example, the following is a partial Human class described through the amount of interfaces it implements.

interface Walking
  method moveLegs(whichSpeed)

interface Breathing
  method inhale()
  method breatheOut(afterHowLong)

class Human implements Walking, Breathing

These kind of interfaces are specially frequent in the DOM world, but there’s no standard de-facto equivalent in the JavaScript one.

No multiple inheritance

The main difference between classes and interfaces, is that instanceof operator won’t tell whenever an object is implementing an interface.

Taking the previous Human class example, a person does not inherit from a behavior or a feature such Walking or Breathing, a person is simply capable of doing those things, and it could do even more, but inherits from just one class: the Human one.

This concept translates in different programming languages into an inability to extend multiple classes if not through single inheritance chain.

However, since it’s clear that is very common to have classes capable of different behaviors, extending multiple interfaces is considered perfectly fine.

A practical approach

Rather than just testing for the presence of a method or a property, JavaScript interfaces could actually implement the behavior, simplifying composition of multiple interfaces, and also making implementation checks easier to understand.

The following is just an idea on interfaces implementation through a function utility.

function implement(target, ...interfaces) {
  const
    // interfaces are uinique and stored as such
    set = new Set(),
    // every interface will augment the new proto
    defineProperties = (proto, iface) => {
      if (!set.has(iface)) {
        set.add(iface);
        Object.defineProperties(
          proto,
          Object.getOwnPropertyDescriptors(iface)
        );
      }
      return proto;
    }
  ;
  // insert between the target and its __proto__ ...
  return Object.setPrototypeOf(
    target,
    // ... a "man-in-the-middle" like object ...
    interfaces.reduce(
      (proto, iface) => {
        // ... configured through all descriptors
        // retrieved from each interface and also
        // from their possibly implemented interfaces too ...
        if (implement.symbol in iface)
          Array.from(iface[implement.symbol])
            .forEach(iface => defineProperties(proto, iface));
        return defineProperties(proto, iface);
      },
      // ... without losing original inheritance ...
      Object.create(
        Object.getPrototypeOf(target),
        {
          [implement.symbol]: {
            configurable: true,
            // ... making analysis at runtime straight forward
            value: set
          }
        }
      )
    )
  );
}

// used to define "man-in-the-middle" Set of interfaces
implement.symbol = typeof Symbol === 'function' ?
  Symbol.for('implements') :
  '__implements__';

// verify if a target implements a specific interface
implement.interface = function (target, iface) {
  // is there an implementation symbol at all?
  while (target && implement.symbol in target) {
    // is the interface registered in the Set?
    if (target[implement.symbol].has(iface)) {
      // good, it's implemented
      return true;
    }
    // climb up the prototypal chain to discover
    // other possibly shadowed inherited Sets
    target = Object.getPrototypeOf(target);
  }
  // not a single Set in the chain had such interface
  return false;
};

Here a very basic usage example:

const
  // Interfaces
  Car = {wheels: 4},
  Plane = {wings: 2},
  // also an interface, composed via interfaces
  FlyingCar = implement({name: 'FlyingCar'}, Car, Plane)
;

[
  FlyingCar.name,   // "FlyingCar"
  FlyingCar.wheels, // 4
  FlyingCar.wings   // 2
].join('\n');

implement.interface(FlyingCar, Car);    // true
implement.interface(FlyingCar, Plane);  // true

Composing interfaces could also just be a matter of shallow copy.

// making FlyingCar an interface too
const FlyingCar = [
  // composing Car and Plane
  Car,
  Plane
].reduce((target, iface) =>
  Object.defineProperties(
    target,
    Object.getOwnPropertyDescriptors(iface)
  ),
  // the FlyingCar interface
  {
    startEngine: function () {
      console.log('ROOOOAAAR');
    }
  }
);

// create the car implementing FlyingCar interface
let myCar = implement({name: 'my flying car'}, FlyingCar);

myCar.startEngine();  // "ROOOOAAAR"
implement.interface(
  myCar,
  FlyingCar
);                    // true

The composed approach works specially well if an interface overwrites an implemented behavior, somehow compromising expectations. For instance, if an object implementing FlyingCar does something different than Car when engine starts, it should probably not be considered an implementation of Car and yet be a perfectly valid FlyingCar.

In all other cases, using just implement should be enough.

const
  Car = {wheels: 4},
  Plane = {wings: 2},
  FlyingCar = implement({}, Car, Plane)
;

let myCar = implement({name: 'my special car'}, FlyingCar);

// the following are all true
implement.interface(myCar, FlyingCar);  // true
implement.interface(myCar, Car);        // true
implement.interface(myCar, Plane);      // true

myCar.name;   // "my special car"
myCar.wheels; // 4
myCar.wings;  // 2

What is special or different about this pattern?

The interfaces layer is a well known, unobtrusive, entity in between a generic object and its prototype. This makes it possible to enrich objects, as well as classes prototypes, without compromising or modifying their inheritance at all.

The target needs to be extensible, but the technique can still be considered somehow unobtrusive, and for the following reasons:

The latter point is basically an operation like the following one:

function dropInterfaces(target) {
  let
    proto = Object.getPrototypeOf(target),
    hOP = Object.prototype.hasOwnProperty
  ;
  // as long as the prototype has interfaces ...
  while (proto && hOP.call(proto, implement.symbol)) {
    // ... grab the next proto in the chain
    proto = Object.getPrototypeOf(proto)
  }
  // return the target without interfaces
  return Object.setPrototypeOf(target, proto);
}

// using the previous myCar object
dropInterfaces(myCar);

myCar.name;   // "my special car"
myCar.wheels; // undefined
myCar.wings;  // undefined

implement.interface(myCar, FlyingCar);  // false

At this point, swapping interfaces is also possible.

function swapInterfaces(implementor, ...interfaces) {
  return implement.apply(
    null,
    [dropInterfaces(implementor)].concat(interfaces)
  );
}

Although, latest two approaches might be considered a bit dirty, fragile, or unnecessary. It’s up to you decide whenever it makes sense to use these patterns.

Also don’t forget Object.preventExtension(implementor) grants inheritance, without affecting already available descriptors in that implementor.

How to test in less modern engines?

Following a quick and dirty polyfill for used Object or Array methods (mostly needed in GJS)

(function (poly) {'use strict';
  // some basic polyfill for global Object
  [
    function setPrototypeOf() {
      // works on GJS, SpiderMonkey, and others
      var set = Object.getOwnPropertyDescriptor(
        Object.prototype,
        '__proto__'
      ).set;
      return {
        configurable: true,
        writable: true,
        value: function setPrototypeOf(target, proto) {
          set.call(target, proto);
          return target;
        }
      };
    },
    function getOwnPropertyDescriptors() {
      var
        defineProperty = Object.defineProperty,
        gOPD = Object.getOwnPropertyDescriptor,
        gOPN = Object.getOwnPropertyNames,
        gOPS = Object.getOwnPropertySymbols,
        ownKeys = gOPS ?
          function (target) {
            return gOPN(target).concat(gOPS(target));
          } :
          gOPN
      ;
      return {
        configurable: true,
        writable: true,
        value: function getOwnPropertyDescriptors(target) {
          return ownKeys(target).reduce(
            function (descriptors, name) {
              var descriptor = gOPD(target, name);
              if (name in descriptors) {
                return defineProperty(
                  descriptors,
                  name,
                  {
                    configurable: true,
                    enumerable: true,
                    writable: true,
                    value: descriptor
                  }
                );
              } else {
                descriptors[name] = descriptor;
                return descriptors;
              }
            },
            {}
          );
        }
      };
    }
  ].forEach(poly, Object);

  // simple poly for Array.from
  // use this polyfill for better results:
  // https://github.com/mathiasbynens/Array.from
  [
    function from() {
      return {
        configurable: true,
        writable: true,
        value: function from(iterable) {
          const out = [];
          for (let value of iterable) {
            out.push(value);
          }
          return out;
        }
      };
    }
  ].forEach(poly, Array);
}(
  function poly(fill) {
    if (fill.name in this) return;
    var descriptor = fill(this);
    if (descriptor)
      Object.defineProperty(this, fill.name, descriptor);
  }
));

Last, but not least, if the [implement.symbol]: bit in the implement function gives you headaches, simply use __implements__: instead, as both property and as implement.symbol.

Once again, this was a proof of concept on how interfaces could work, not an official proposal or a proper, fully tested, library.

Last, but not least, everything mentioned or explained in this post has been also explained in my JS glossary on demand book.

Thanks for reading.

Andrea Giammarchi

Fullstack Web Developer, Senior Software Engineer, Team Leader, Architect, Node.js, JavaScript, HTML5, IoT, Trainer, Publisher, Technical Editor