In this blog post I will introduce the concept of interfaces and how they can be useful even in dynamic languages. I will also use the library Implement.js to bring the concept to JavaScript, and show you how to get some extra utility out of interfaces.
Google defines an interface as “a point where two systems, subjects, organisations, etc. meet and interact,” and this definition holds true for interfaces in programming. In software development, an interface is a structure that enforces specific properties on an object — in most languages this object is a class.
Here’s an example of an interface in Java:
In the above example, the Car
interface describes a class that has two methods with no return type, both of which take a single integer argument. The details of the implementation of each function is left up to the class, this is why the methods both have no body. To ensure a class implements the Car
interface, we use the implements
keyword:
Interfaces are not a thing in JavaScript, not really anyway. JavaScript is a dynamic language, one where types are changed so often that the developer may not have even realised, because of this people argue there is no need for an interface to be added to the ECMAScript standard that JavaScript is based on.
However, JavaScript has grown massively as a backend language in the form of Node.js, and with that comes different requirements and a different crowd who may have a different opinion. To add to this, the language is quickly becoming the front-end tool; it has got to the point where many developers will write the vast majority of their HTML inside .js files in the form of JSX.
So as the language grows to take on more roles, it is helpful then to make sure one of our most crucial data structures is what we expected it to be. JavaScript may have the class
keyword, but in truth this is just an uninstantiated constructor function, and once called it is simply an object. Objects are ubiquitous, so it is sometimes beneficial to ensure they match a specific shape.
Recently at work I found a case where a developer was expecting a property returned as part of an API response to be true
but instead got "true"
, causing a bug. An easy mistake, and one that could also have been avoided if we had an interface.
Interfaces, with a few minor modifications, could be used to reshape objects. Imagine implementing a “strict” interface, where no properties outside of the interface are permitted, we could delete or rename these properties, or even throw an error if we encounter them.
So now we have an interface that will tell us when we are missing certain properties, but also when we have unexpected properties, or if the properties’ types are not what we expect. This adds other possibilities, say for example refactoring a response from an API while adding that extra layer of safety on top of the interface’s standard behaviour. We could also use interfaces in unit tests if we have them throw errors.
Implement.js is a library that attempts to bring interfaces to JavaScript. The idea is simple: define an interface, define the types of it’s properties, and use it to ensure an object is what you expect it to be.
First install the package:
npm install implement-js
Next, create a .js file and import implement
, Interface
, and type
:
To create an interface, simply call Interface
and pass in a string as the name of your interface — it’s not recommended, but if you omit the name a unique ID will generated. This returns a function that accepts an object where the properties are all type
objects, a second argument can also be passed with options to show warnings, throw errors, delete or rename properties, ensure only the properties of the interface are present, or to extend an existing Interface
.
Here’s an interface that describes a car:
It has a seats
property that should be of type number, a passengers array that contains objects which must themselves implement the Passenger
interface, and it contains a beep
property, which should be a function. The error
and strict
options have been set to true, meaning that errors will be thrown when a property of the interface is missing and also when a property not on the interface is found.
Now we want to implement our interface, in a simple example we will create an object using an object literal and see if we are able to implement our interface.
First, we create a Ford
object, then we will attempt to implement it against our Car
interface:
As we see from the comment above, this throws an error. Let’s look back at our Car
interface:
We can see that while all the properties are present, strict mode is also true, meaning that the additional property fuelType
causes an error to be thrown. Additionally, although we have a passengers
property, it is not an array.
In order to correctly implement the interface, we remove fuelType
and change the value of passengers
so that it is an array containing objects that implement the Passenger
interface:
It’s true that while interfaces are typically associated with object-oriented languages, and JavaScript is a multi-paradigm language that uses prototypal inheritance, interfaces can still be highly useful.
For example, using implement-js
we can easily refactor an API response while ensuring it has not deviated from what we expect. Here’s an example used in conjunction with redux-thunk
:
First, we define the TwitterUser
interface, which extends the User
interface, as an object with twitterId
and twitterUsername
properties. trim
is true meaning that we will discard any properties not described on the TwitterUser
interface. Since our API returns properties in an unfriendly format, we have renamed the properties from twitter_username
and twitter_id
to camelcase versions of themselves.
Next, we define an async action with redux-thunk
, the action triggers an API call, and we use our TwitterUser
interface to discard properties we don’t want and to ensure it implements the properties we expect, with the correct types. If you prefer to keep your action creators pure(er) or don’t use redux-thunk
, you may want to check the interface inside twitterService.getUser
and return the result.
Note: when extending an interface options are not inherited
Unit tests are also a suitable place to use interfaces:
We have seen how interfaces can be useful in JavaScript: even though it is a highly dynamic language, checking the shape of an object and that it’s properties are a specific data type gives us an extra layer of safety we otherwise would be missing out on. By building on the concept of interfaces and using implement-js
we have also been able to gain additional utility on top of the added security.