As of ES2015, new ECMAScript standard library APIs have used a protocol-based design, enabled by the introduction of Symbols. Symbols are ECMAScript values which have identity and may be used as object property keys. The goal of this proposal is to provide a convenient syntactic facility for protocol-based design.
The proposal has not yet been brought before TC39 and is therefore Stage 0. It is on the July 2017 agenda.
interface InterfaceName {
// declare a symbol which much be implemented
thisMustBeImplemented;
// and some methods that you get for free by implementing this interface
youGetThisMethodForFree(...parameters) {
methodBody;
}
}
class ClassName implements InterfaceName {
[InterfaceName.thisMustBeImplemented]() {
// this is the implementation for this class
}
}
let instance = new ClassName;
instance.youGetThisMethodForFree();
A prototype using sweet.js is available at https://github.com/disnet/sweet-interfaces
The most well-known protocol in ECMAScript is the iteration protocol. APIs such
as Array.from
, the Map
and Set
constructors, destructuring syntax, and
for-of
syntax are all built around this protocol. But there are many others.
For example, the protocol defined by Symbol.toStringTag
could have been
expressed using interfaces as
interface ToString {
tag;
toString() {
return `[object ${this[ToString.tag]}]`;
}
}
Object.prototype[ToString.tag] = 'Object';
Object implements ToString;
The auto-flattening behaviour of Promise.prototype.then
was a very controversial decision.
Valid arguments exist for both the auto-flattening and the monadic versions to be the default.
Interfaces eliminate this issue in two ways:
- Symbols are unique and unambiguous. There is no fear of naming collisions, and it is clear what function you are using.
- Interfaces may be applied to existing classes, so there is nothing preventing consumers with different goals from using their own methods.
// Applicative elided here
interface Monad extends Applicative {
bind;
}
class Identity {
constructor(val) { this.val = val; }
unwrap() { return this.val; }
}
Promise.prototype[Monad.bind] = function (f) {
this.then(function(...args) {
return new Identity(f.apply(this, args));
}).unwrap();
}
Promise implements Monad;
Finally, one of the biggest benefits of interfaces is that they eliminate the fear of mutating built-in prototypes. One of the beautiful aspects of ECMAScript is its ability to extend its built-in prototypes. But with the limited string namespace, this is untenable in large codebases and impossible when integrating with third parties. Because interfaces are based on symbols, this is no longer an anti-pattern.
class Ordering {
static LT = new Ordering;
static EQ = new Ordering;
static GT = new Ordering;
}
interface Ordered {
compare;
lessThan(other) {
return this[Ordered.compare](other) === Ordering.LT;
}
}
String.prototype[Ordered.compare] = function() { /* elided */ };
String implements Ordered;
Interfaces may extend other interfaces. This expresses a dependency relationship between the interfaces.
interface A { a; }
interface B extends A { b; }
class C implements B {
[A.a]() {}
[B.b]() {}
}
class D implements A {
[A.a]() {}
}
In the example above, notice how B extends A and any class that implements B must also implement A.
If a class that is implementing an interface is missing some of the required fields, it will fail at class definition time. This program will throw:
interface I { a; b; }
class C implements I {
[I.a]() {}
// note the missing implementation of I.b
}
Minimal implementations can be expressed using interface inheritance.
// Applicative elided
interface Monad extends Applicative {
bind;
join;
kleisli() {}
}
// two possible minimal implementations for Monad
interface MonadViaBind extends Monad {
[Monad.join]() { /* default implementation in terms of bind elided */ }
}
interface MonadViaJoin extends Monad {
[Monad.bind]() { /* default implementation in terms of join elided */ }
}
class C implements MonadViaBind {
[Monad.bind]() {}
}
class D implements MonadViaJoin {
[Monad.join]() {}
}
An important aspect of this proposal is that it needs to be possible to apply an interface to an existing class.
interface Functor {
map;
}
Array.prototype[Functor.map] = Array.prototype.map;
Array implements Functor;
The implements
operator returns its left operand to allow for chaining.
C implements A implements B;
is equivalent to
C implements A;
C implements B;
Some interfaces require their methods to be put on the constructor instead of
the prototype. Use the static
modifier for this.
interface A {
static b() {}
}
class C implements A { }
C.b();
Similarly, require an interface field to be on the constructor instead of the
prototype using the static
modifier.
interface Monoid {
concat;
static identity;
}
See the tests in https://github.com/disnet/sweet-interfaces/tree/master/test for specific details about the proposal.
- Should method names be symbols like fields?
- Relatedly, if we stick with strings, what do we do about method names that conflict with existing ones on the implementer?
- Do we want to have a way to query whether a class implements an interface?
export interface ...
form?
TODO
TODO
TODO
class A extends mixin(FeatureA, FeatureB) {}
TODO
TODO