Skip to content

tc39/proposal-first-class-protocols

ECMAScript First-Class Protocols Proposal

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.

Stage: 1

Champions:

  • Michael Ficarra (@michaelficarra)
  • Lea Verou (@leaverou)

Contents

  1. What does it look like?
    1. Implementing protocols on objects
  2. Motivation
  3. Syntax in depth
    1. Querying protocol membership
    2. Providing explicit member names
    3. Providing non-method data properties
    4. Interaction with private names
  4. Protocol composition
    1. Sub-protocols
    2. Inheritance
  5. Imperative API
    1. The Protocol() constructor
    2. Protocol introspection: Protocol.describe()
    3. Protocol flattening: Protocol.union()
  6. Convenience Features
    1. Specifying and implementing protocols on constructors
    2. Automatically creating string aliases for all provided members
  7. Patterns
    1. Conflict resolution & disambiguation
  8. How can I play with it?
  9. Relationship to similar features
    1. Haskell type classes
    2. Rust traits
    3. Java 8+ interfaces
    4. Ruby mixins
    5. ECMAScript mixin(...) pattern
  10. Links to previous related discussions/strawmen
  11. History
  12. Changelog
    1. Feb 24, 2026
    2. From the 2018 update

What does it look like?

The syntax for declaring a protocol looks like this:

protocol Foldable {
  requires foldr;

  // provided members
  toArray() {
    return this[Foldable.foldr]((m, a) => [a].concat(m), []);
  }
  get length() {
    return this[Foldable.foldr](m => m + 1, 0);
  }
}

Important

An alternative to the requires keyword is abstract. See issue #50.

Required members are defined by the requires keyword. Any other member is provided. Protocols can have only required members, only provided members, or both.

Despite the syntactic similarity to class elements, the names of protocol members are actually symbols, which ensures uniqueness and prevents name collisions. E.g. in this example, the required member is not a "foldr" property, but a Foldable.foldr symbol, and the two methods provided will not be added to classes as "toArray" or "length" properties, but as Foldable.toArray and Foldable.length symbols.

Implementing protocols on objects

Once a protocol is declared, it can be implemented on any object that satisfies the protocol's requirements through a Protocol.implement() method.

Important

Currently the only constraint implied by requires is that the property is present. See issue #4 for discussion on additional constraint types.

Implementing a protocol on an object is equivalent to copying the protocol's members to the object.

let obj = {
  [Foldable.foldr](f, memo) {
    // implementation elided
  }
}
Protocol.implement(obj, Foldable);
//=> obj[Foldable.toArray] and obj[Foldable.length] are now available

If the object already has a property that would be provided by the protocol, it is not replaced.

Motivation

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 protocols as

protocol ToString {
  requires tag;

  toString() {
    return `[object ${this[ToString.tag]}]`;
  }
}

Object.prototype[ToString.tag] = 'Object';
Protocol.implement(Object.prototype, 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. Protocols eliminate this issue in two ways:

  1. Symbols are unique and unambiguous. There is no fear of naming collisions, and it is clear what function you are using.
  2. Protocols may be applied to existing classes, so there is nothing preventing consumers with different goals from using their own methods.
protocol Functor {
  requires map;
}

class Identity {
  constructor(val) { this.val = val; }
  unwrap() { return this.val; }
}

Promise.prototype[Functor.map] = function (f) {
  return this.then(function(x) {
    if (x instanceof Identity) {
      x = x.unwrap();
    }
    let result = f.call(this, x);
    if (result instanceof Promise) {
      result = new Identity(result);
    }
    return result;
  });
};

Protocol.implement(Promise.prototype, Functor);

Finally, one of the biggest benefits of protocols 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 protocols 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;
}

protocol Ordered {
  requires compare;

  lessThan(other) {
    return this[Ordered.compare](other) === Ordering.LT;
  }
}

String.prototype[Ordered.compare] = function() { /* elided */ };
Protocol.implement(String.prototype, Ordered);

Syntax in depth

Querying protocol membership

An implements operator can be used to query protocol membership, by checking whether an object satisfies a protocol's requirements and includes its provided members.

if (obj implements P) {
  // reached iff obj has all fields
  // required by P and all fields
  // provided by P
}

Providing explicit member names

By default, both provided and required member names actually define symbols on the protocol object, which is a key part of how protocols avoid conflicts. It is possible to provide an explicit member name that will be used verbatim, by using ComputedPropertyName syntax:

protocol P {
  requires ["a"];
  b(){ print('b'); }
}

class C implements P {
  a() {}
}

C.prototype implements P; // true
(new C)[P.b](); // prints 'b'

This makes it possible to describe protocols already in the language which is necessary per committee feedback. This includes protocols whose required members are strings, such as thenables, as well as protocols whose required members are existing symbols, such as the iteration protocol:

protocol Iterable {
  requires [Symbol.iterator];

  forEach(f) {
    for (let entry of this) {
      f.call(this, entry);
    }
  }

  // ...
}

Providing non-method data properties

As seen throughout the rest of this explainer, protocols may provide methods and accessors using the same notation used within class declarations. In addition to those, protocols may provide data properties with any value using the following notation:

protocol P {
  x = 0
}

In the above example, protocol P provides a member named x with value 0. This differs from the meaning of the similar construct within class declarations, where it would define a class field with a field initializer. Class field initializers are expressions that are evaluated each time a class is instantiated, whereas the expression following = in the protocol above is evaluated only once, during the protocol evaluation.

Interaction with private names

A protocol that provides properties with private names which are only available to reference by other protocol members seems clearly useful. But this proposal is still valuable without support for private names. For now, private names in protocols are an early error. Private names in protocols can and should be pursued as a follow-up proposal. This topic is being tracked in #66.

Protocol composition

Sub-protocols

A required member can also be required to implement one or more sub-protocols, specified inline or by reference.

This can be used to specify static members on protocols meant to be used on classes:

protocol Foldable {
  requires foldr;

  // provided members
  toArray() {
    return this[Foldable.foldr]((m, a) => [a].concat(m), []);
  }
  get length() {
    return this[Foldable.foldr](m => m + 1, 0);
  }

  requires ["constructor"] implements protocol {
    from () { /* elided */ }
  }
}

class C implements Foldable {
  [Foldable.foldr](f, memo) {
    // implementation elided
  }
}

//=> C.prototype.constructor[Foldable.from] is now available
// Therefore, C[Foldable.from] is now available

Important

Actually, Foldable.from would not be available. This is an open design dilemma, see #81 for discussion.

Important

Should constructor and prototype be always implicitly strings and not create symbols on the protocol object? See issue #84

Inheritance

Once created, protocols are frozen and cannot be modified. Instead, inheritance can be used to create new protocols from existing ones. The syntax and semantics are similar to classes:

protocol A { requires a; }
protocol B extends A { requires b; }

class C implements B {
  [B.a]() {}
  [B.b]() {}
}

// or

class C implements A, B {
  [A.a]() {}
  [B.b]() {}
}

Important

See issue #23 for discussion on the exact implementation and semantics of protocol composition.

Imperative API

In addition to Protocol.implement() which has been described earlier, a few other imperative API methods are available.

The Protocol() constructor

Protocols can also be constructed imperatively, via the Protocol() constructor. All options are optional.

const Foldable = new Protocol({
  name: 'Foldable',
  extends: [ ... ],
  members: {
    foldr: { required: true },
    toArray: {
      value: function () { ... },
    },
    length: {
      get: function () { ... },
      set: function (value) { ... },
    },
    contains: {
      value: function (eq, e) { ... },
    },
  }
});

Important

The exact shape is TBD (see #82). One design decision that affects it is whether "foo" and foo are distinct members (see #59).

Protocol introspection: Protocol.describe()

Protocol.describe(p) takes an existing protocol object and returns an object literal that could be passed to the constructor to create a new protocol.

const P = Protocol.describe(Foldable);
// => {
//   name: 'Foldable',
//   members: {
//     foldr: { required: true },
//     toArray: {
//       value: function () { ... },
//     },
//     length: {
//       get: function () { ... },
//       set: function (value) { ... },
//     },
//     contains: {
//       value: function (eq, e) { ... },
//     },
//   }
// }

Protocol flattening: Protocol.union()

Protocol.implement() is atomic, and will throw an error if the object does not satisfy the protocol's requirements. However, when implementing multiple protocols on a single object, some of their requirements may be satisfied by other protocols. Consider this example:

protocol A { requires ["a"]; ["b"]() {} }
protocol B { requires ["b"]; ["c"]() {} }
protocol C { requires ["c"]; ["a"]() {} }

let obj = {};

All of the following calls will throw:

  • Protocol.implement(obj, A);
  • Protocol.implement(obj, B);
  • Protocol.implement(obj, C);

There is no order in which we can call Protocol.implement() on obj to avoid errors, even though all their requirements are satisfied by another protocol in the group.

To address this, we can combine protocols into a single protocol by taking a deep union of all required and provided members. The resulting protocol satisfies implements checks for all protocols in the group, but requirements are checked for the entire set, rather than for each protocol individually.

Protocol.implement(obj, Protocol.union(A, B, C)); // no errors!

Note

A protocol may require and provide the same member name. This does not throw an error, the requirement is simply trivially satisfied. This can easily happen when flattening protocols, e.g. in the example above, Protocol.union(A, B, C) will result in a protocol that requires all its provided members.

Convenience Features

Specifying and implementing protocols on constructors

In addition to Protocol.implement(), which works for any object, constructors support declaratively implementing protocols on their prototype via the implements keyword:

class C implements Foldable {
  [Foldable.foldr](f, memo) {
    // implementation elided
  }
}


//=> C.prototype[Foldable.toArray] and C.prototype[Foldable.length] are now available
//=> C.prototype implements Foldable === true
let c = new C();
//=> c implements Foldable === true

When protocols are implemented on constructors (via the class C implements P syntax), they are installed on the class .prototype object, i.e. they are equivalent to Protocol.implement(C.prototype, P).

By implementing Foldable, class C now gained a C.prototype[Foldable.toArray] method and a C.prototype[Foldable.length] accessor, which it can choose to expose to the outside world like so:

class C implements Foldable {
  [Foldable.foldr](f, memo) {
    // implementation elided
  }

  get toArray() {
    return this[Foldable.toArray];
  }

  get length() {
    return this[Foldable.length];
  }
}

Multiple protocols can be implemented by passing a list of protocols to the implements keyword. In that case, protocols are wrapped in Protocol.union() before being passed to Protocol.implement(), to ensure that only real requirements propagate to the constructor itself.

class C implements P, Q { /* ... */ }

// Equivalent to:
class C { /* ... */ }
Protocol.implement(C.prototype, Protocol.union(P, Q));

Automatically creating string aliases for all provided members

While symbol-based names are the default (and desirable for avoiding conflicts), many use cases require integration with implementors of existing ad hoc protocols that are defined in terms of regular string-named properties. By default, the implementing object needs to define mappings between the symbol-based names the protocol defines and the string-based names it wants to expose, which can be tedious.

For example, the web platform supports making custom elements form-associated which is currently implemented as a delegation pattern over an ElementInternals object. If protocols existed, one could imagine a protocol like this provided by the browser:

protocol FormAssociated {
  requires formValue;

  get form() { /* elided */ }
  get labels() { /* elided */ }
  get validationMessage() { /* elided */ }
  get validity() { /* elided */ }
  get willValidate() { /* elided */ }
  checkValidity() { /* elided */ }
  reportValidity() { /* elided */ }
  setCustomValidity(message) { /* elided */ }
  setValidity(validity) { /* elided */ }
  // ...
}

And then used like this:

class MySlider implements FormAssociated {
  get [FormAssociated.formValue]() {
    return this.value;
  }
  set [FormAssociated.formValue](value) {
    this.value = value;
  }
}

However, usually implementations will want to expose all the provided members, in order to be consistent with built-in elements. This involves a lot of boilerplate:

class MySlider implements FormAssociated {
  get [FormAssociated.formValue]() { /* elided */ }
  set [FormAssociated.formValue](value) { /* elided */ }

  get labels () { return this[FormAssociated.labels]; }
  get validationMessage () { return this[FormAssociated.validationMessage]; }
  get validity () { return this[FormAssociated.validity]; }
  get willValidate () { return this[FormAssociated.willValidate]; }
  get checkValidity () { return this[FormAssociated.checkValidity]; }
  get reportValidity () { return this[FormAssociated.reportValidity]; }
  get setCustomValidity () { return this[FormAssociated.setCustomValidity]; }
  get setValidity () { return this[FormAssociated.setValidity]; }
  // ...
}

While protocols could define all their provided members as string-named properties, this moves control from the object to the protocol, which is not always desirable. In some cases protocols are implemented to add functionality that is primarily meant to be used internally, while in other cases they are meant to also add API that exposes this functionality to consumers.

To allow for object authors to make that call, protocol syntax supports a convenience syntax that automatically creates string aliases for all provided members as accessors on the implementing object.

The exact syntax is TBD, discussed in issue #47.

Some (not necessarily mutually exclusive) ideas include:

  • an extension to the implements syntax (e.g. class C implements Foldable with strings)
  • a Protocol.implement() option (e.g. Protocol.implement(obj, Foldable, { withStrings: true }))
  • a method that transforms a protocol into another protocol that also provides string aliases for all provided members (e.g. class C implements Protocol.withStrings(P))

Patterns

Conflict resolution & disambiguation

While protocols are designed to avoid conflicts through the use of symbols, there are cases where conflicts can arise, such as:

  • Explicit string-based member names
  • External symbols
  • When using automatic string aliases

By default, protocols with conflicting members will throw an error.

protocol A {
  ["x"] = 1
}

protocol B {
  ["x"] = 2
}

class C implements A, B {}
//=> Error: Protocol member "x" is defined in multiple protocols: A and B

However, as discussed earlier, properties in the implementing object take precedence over protocol members. As a corollary, this also provides a way for the implementing object to disambiguate:

class C implements A, B {
  get x() {
    let Ax = Protocol.describe(A).members.x.value;
    let Bx = Protocol.describe(B).members.x.value;
    return Math.max(Ax, Bx);
  }
}
//=> C.prototype.x === 2

How can I play with it?

An outdated prototype using sweet.js is available at https://github.com/disnet/sweet-interfaces. It needs to be updated to use the latest syntax. A polyfill for the runtime components is available at https://github.com/michaelficarra/proposal-first-class-protocols-polyfill.

Relationship to similar features

Haskell type classes

This proposal was strongly inspired by Haskell's type classes. The conceptual model is identical aside from the fact that in Haskell the type class instance (essentially an implicit record) is resolved automatically by the type checker. For a more Haskell-like calling pattern, one can define functions like

function fmap(fn) {
  return function (functor) {
    return functor[Functor.fmap](fn);
  };
}

Similar to how each type in Haskell may only have a single implementation of each type class (newtypes are used as a workaround), each class in JavaScript may only have a single implementation of each protocol. Haskell programmers get around this limitation through the use of newtypes. Users of this proposal will extend the protocol they wish to implement with each possible alternative and allow the consumer to choose the implementation with the symbol they use.

Haskell type classes exist only at the type level and not the term level, so they cannot be passed around as first class values, and any abstraction over them must be done through type-level programming mechanisms. The protocols in this proposal are themselves values which may be passed around as first class citizens.

Rust traits

Rust traits are very similar to Haskell type classes. Rust traits have restrictions on implementations for built-in data structures; no such restriction exists with this proposal. The implements operator in this proposal would be useful in manually guarding a function in a way that Rust's trait bounds do. Default methods in Rust traits are equivalent to what we've called methods in this proposal.

Java 8+ interfaces

Java interfaces, as of Java 8, have many of the same features as this proposal. The biggest difference is that Java interfaces are not ad-hoc, meaning existing classes cannot be declared to implement interfaces after they've already been defined. Additionally, Java interfaces share the member name namespace with classes and other interfaces, so they may overlap, shadow, or otherwise be incompatible, with no way for a user to disambiguate.

Ruby mixins

Ruby mixins are similar to this proposal in that they allow adding functionality to existing classes, but different in a number of ways. The biggest difference is the overlapping/conflicting method names due to everything existing in one shared namespace. Another difference that is unique to Ruby mixins, though, is that they have no check that the methods they rely on are implemented by the implementing class.

ECMAScript mixin(...) pattern

class A extends mixin(SuperClass, FeatureA, FeatureB) {}

This mixin pattern usually ends up creating one or more intermediate prototype objects which sit between the class and its superclass on the prototype chain. In contrast, this proposal works by copying the provided protocol methods into the class or its prototype. This proposal is also built entirely off of Symbol-named properties, but doing so using existing mechanisms would be tedious and difficult to do properly. For an example of the complexity involved in doing it properly, see the output of the sweet.js implementation.

Links to previous related discussions/strawmen

History

Changelog

Feb 24, 2026

  • Removed static members
  • Added sub-protocols
  • Edited constructor signature to represent current thinking
  • Added Protocol.describe()

From the 2018 update

  • Removed the implements ClassElement syntax (#56)
  • Explicit member names now use ComputedPropertyName syntax (#48)
  • Added explicit requires keyword (#50)

About

a proposal to bring protocol-based interfaces to ECMAScript users

Resources

License

Code of conduct

Contributing

Security policy

Stars

Watchers

Forks