JavaScript traits: the clean way to modify global prototypes

With ES6 it’s finally possible to add methods to Object.prototype, Array.prototype and all the others, in a clean way

Paolo Giangrandi
Published in
8 min readDec 17, 2018

--

Traits are a feature used by most modern languages to implement polymorphism without relying on inheritance.

We’ll introduce them, show which problems they would solve in JavaScript, and suggest a way to implement them. Hope you find it interesting!

A few words about JavaScript and modern languages

JavaScript has been my language of choice for the past 5 years in spite of its infamous quirks.

It’s fast to write, efficient to run, but above all else, it is modern. The language itself as well as its ecosystem uses great features and solutions, the best ones we know of.

JavaScript supports Object Oriented Programming and uses duck typing to achieve polymorphism. This approach is more powerful and flexible than inheritance, but it still has some major flaws.

A better, more modern way to implement OOP is using traits. Implementing decent traits in JavaScript through duck typing and prototypal inheritance used to have problems, but since a recent version of the language (ECMAScript 6) added a new primitive data type (symbol), the situation has improved.

Very few are using this exciting feature of the language though, and that’s what motivated this article.

What are traits?

Traits are a way to add semantics to existing types without risking unintentional interference with existing code.

Traits are an alternative to inheritance as a method for implementing polymorphism, and all the modern languages have them: think of go, haskell and rust just to name a few.

JavaScript is a prototypal, duck typed language, and this has always allowed programmers to do something very similar just by adding a new property to an existing prototype. But this simple addition has major problem and should be avoided. In fact you should never modify the prototype of types you don’t own, and that’s why most libraries go out of their way to offer new functionalities to existing types without actually modifying those types.

Recent versions of JavaScript (i.e. ECMAScript 6) added a new primitive data type, symbol, which can be used to implement traits effectively. A symbol is basically a unique identifier that can be used as a property and that will never collide with anything else. In ECMAScript 6 they needed a way to expand standard types without breaking compatibility with existing code. That is why they introduced symbol, and they're indeed using it to implement traits. However, instead of advertising this new feature and making traits a first class citizen, they have let symbols remain in the shadows. The standard calls this feature protocol, instead of trait, and one of several examples is the iteration protocol.

Besides the lack of guidelines and good examples, another issue makes it tough to use symbols as traits: a lack of good syntax to do so.

But enough talking! Let’s look at an example.

Why do we need traits? An example

Imagine that you need a serializer to convert pieces of data into a string that can be stored somewhere.

JSON.stringify() is not good enough, as it doesn't support "complex" objects (try to stringify a circular object, a RegExp, a Map etc, and you'll see).

Well, let’s write our own serialization function then. We want to support primitive data types (boolean, number, string etc), built-in types (Array, Map, RegExp etc), as well as the classes we or somebody else define. Something might be impossible to serialize, like Functions or Promises, and that's OK: we'll just throw an error if we're given to serialize one of these.

What we are trying to do, is to add a new serializing logic to most types, regardless of whether they’re defined by us, by other people, or built-in in the language. This logic needs to be custom-written for each type.

We can achieve that by adding a new serialize method to everything. This way var.serialize() will return a serializable representation for any variable var.

Let’s try it out…

It prints Person("peoro", 32){objects:[{"a": true,"re": /^...$/g}]}, which is exactly what we wanted.

Amazing! Isn’t it?

Well, not really.

We modified existing types we have no ownership of. That’s called monkey patching. It seems to work, but will give many serious problems as soon as somebody tries to use our serializer in a real application. Let’s see a few:

  • Try to serialize the object {serialize:true}: you'll get an error, since the serialize property of such object overrides Object.prototype.serialize.
  • Somebody else might define a different serialize method to serialize objects in a different format. Our serializer and theirs will be incompatible; if both are loaded in the same project (even as an indirect dependency) things will break in unexpected ways.
  • Try to iterate using for...in on a plain object: you'll iterate over the Object.prototype's serialize property as well. This is gonna break the majority of existing code.

The problem with monkey patching is that we’re modifying global data: any function that wasn’t written by us might rely on assumptions on existing objects that we might have broken,

This is the reason why libraries (including the huge ones that could impose their own standards — think of jQuery or lodash) won’t modify built-in types. They would rather expose free functions (like lodash), or wrappers to encapsulate existing objects and add new methods only to their wrappers (like jQuery).

It’s important to note that the solutions chosen by these libraries are quite limited: it’s hard to specialize the behavior for wrappers and free functions. When you write them, you might hardcode a waterfall of ifs to support a bunch of types, but later it cannot be extended. You won't be able to make their functions work with your custom types.

As an exercise, try to define a serialize function able to serialize several types without modifying existing objects nor their prototype. Then try to make it possible for the users of your library to add support for their own types, or to existing third-party objects. Most of the solutions you might consider (e.g. using a Type → serializationFunction map) would likely result in further unexpected problems.

This is where symbols come in our assistance. Welcome to the world of traits.

How to implement traits in JavaScript

A symbol is a primitive type introduced in ES6 which can be used as an object's key, and it's guaranteed to never ever clash with anything else: if sym is a symbol, the only way to access object[sym] is by using sym itself. Besides, for...in loops won't iterate over symbols.

It’s not a coincidence that symbols work this way: they were added to the standard for the exact same reason why we need them. ECMAScript 6 wanted to add new functionalities to existing types, but, as we've seen, it was impossible to do so without risking to break existing code.

For an example of how the standard is using them, look at the iterable protocol. A new symbol Symbol.iterator was introduced. The types that implement it can be iterated over using the for...of syntax. Such symbol is implemented for Array, TypedArray, String, Map, Set, and you can implement it on your own types as well.

Our serializer should instantiate a serialize symbol, use it, and expose it for everybody to use:

Our users can then implement the same serialize symbol on their types and use it. No other existing piece of code will be affected.

symbols aren't the final answer though... They're very powerful and are used by the standard to implement traits, but the amount of documentation covering them is miniscule. Virtually no guidelines, very few tutorials or articles explaining what they are and how to use them. The result is that very few modules are using them.

A further problem concerns their syntax: there’s no special syntax to use them, and in some cases this becomes a pain.

Imagine a lodash-traits library that offers and implement a symbol for each lodash function.

You wouldn’t be able to just do:

You’d have to do…

And this becomes uncomfortable pretty fast.

This is why, in addition to traits, we’re proposing a new syntax, designed to aid trait development.

A better syntax for traits

We’re proposing a language extension, the straits syntax, to be able to write the previous snippet the following way:

What does it mean?

use traits * from traitSet; means that we will be looking for symbols inside the object traitSet. We call traitSet a trait set. object.*key means that we're accessing object with the symbol called key found in the trait set in use.

In pratice:

Is roughly equivalent to:

We would write the serialization part of our module the following way:

This syntax is compatible with the standard symbols built-in in ECMAScript 6:

It’s possible to use traits from multiple trait sets at the same time, and we’ll receive an error in case a trait is duplicated or missing.

This syntax is meant to…

  • Turn symbols into first class citizens of JavaScript.
  • Make traits easier both to declare and use.
  • Avoid conflicts and mistakes between variables in scope and traits.

It’s currently possible to develop code using this syntax, and to convert it into standard JavaScript using a babel plugin: @straits/babel.

Article conclusion and straits introduction

Hopefully it’s now clear why there’s a need for traits, how to create them using symbols, prototypal inheritance and duck typing, and how to use them comfortably.

The straits project offers a number of common functions to aid in the declaration and usage of symbols, traits and trait sets.

If you want to give it a chance, just run npm init @straits in an empty directory: it will set up a project ready to use the new syntax. Then run npm install and everything will be ready: npm start will run src/index.js, a hello-world ready to be played with.

If you like traits, you should just use the straits syntax in your project. It will be completely transparent to your users, since the code you’ll release or publish on npm will be transpiled: standard, regular JavaScript. The users of your module are free to choose whether they want to use use this syntax as well, or rather use symbols manually or even through free-functions. Give a look at lodash-traits’ test/index.js to see how a module using traits can be used with or without using the straits syntax.

If you want to give a look at some projects relying on traits, check out:

  • lodash-traits: a trait set wrapping lodash functions.
  • chalk-traits: a trait set wrapping chalk functions.
  • Scontainers: a powerful, high performance library (although still in alpha) to work with collections of data.
  • ESAST: a library to manipulate JavaScript AST in a comfortable way (i.e. without wrapper objects).

Originally published at straits.github.io.

--

--

Writer for

I love monkeys, public transports, Internet, Berlin and tons of other random things.