First-class protocols

Update on current status and design challenges

## History - **2017**: Original design by Michael Ficarra - **2017**: Stage 1 - **2018**: Update - **2019-2024**: 🦗🦗🦗 - **Nov 2025**: Renewed interest - **Jan 2026**: New co-champions: Lea Verou, Jordan Harband - **Mar 2026**: Big update!
## Today's Agenda - Reminders: getting back up to speed with the proposal - Changes since 2018 and current status - Unresolved design challenges - Open discussion and committee feedback
## Motivation - Reify existing built-in protocols like iteration - Allow JS authors to describe and refer to their program's interfaces - Use a standardised representation so they can be consistently introspected and composed
## Design Principles - Encourage Symbol-based interfaces - No coordination needed (between producers and consumers, or across multiple producers) - All the fun and convenience of monkey-patching built-ins, none of the downsides! - Enable a more principled duck typing
## Real-world use cases ### Reify existing built-in protocols - Both symbol-based and string-based - Iteration, Generators - Express capability to convert to… - …a string (`toString`) - …a primitive (`valueOf`, `Symbol.toPrimitive`) - …JSON (`toJSON`) - `Object.prototype.toString()` (`Symbol.toStringTag`) - Thenables, `Symbol.isConcatSpreadable`, `Symbol.unscopables`, regular expression stuff, `Symbol.species` 😱, ... - ArrayLike, SetLike, MapLike, etc.
## Real-world use cases ### New built-in protocols - Mathematical properties of structures - Algebraic structures: groups, lattices, rings, algebras, etc. - Category theoretic structures - StructuredCloneable, Ordered, Equals, FromIterator, etc. - Symbol-based alternatives of existing string-based protocols - Implementations of these for JS built-ins - New protocols for operator overloading?!
## Real-world use cases ### DOM & Web Components - `EventTarget` could have been a protocol! - `HTMLMediaElement` could have been a set of protocols (WithControls, SupportsPlayback etc.) - Element capabilities: Focusable, Labelable, PopoverTarget, FormAssociated, etc - Web Components - `ElementInternals` could have been a set of protocols - Also: WithStates, [WithStyles](https://lit.dev/docs/components/styles/), WithTemplate, etc
## [Design circa 2018](https://github.com/tc39/proposal-first-class-protocols/blob/2eb25b2913888f06685f392244212121e618ed14/README.md)
### Declaring a protocol ```js protocol Foldable { // No value = required member // Looks like a string, actually a symbol foldr; // provided members toArray() { return this[Foldable.foldr]((m, a) => [a].concat(m), []); } get length() { return this[Foldable.foldr](m => m + 1, 0); } } ```
### Implementing a protocol ```js class C implements Foldable { [Foldable.foldr](f, memo) { /* ... */ } } ``` ```js C.prototype[Foldable.foldr] = function(f, memo){ // elided } Protocol.implement(C, Foldable); ``` ```js class C { implements Foldable { foldr(f, memo) { // elided } } } ```
## Design c. 2018: Summary - Protocol declarations/expressions - Declare as identifiers, implemented as symbols - Distinct required/provided members - `implements` operator - `new Protocol({ ... })` constructor - Integration with class heads - Inline, grouped implementations in protocol/class bodies - `Protocol.implement(obj, P)`

What has changed

## Explicit required members
### Before ```js protocol P { // Just a name: required foo; // Method or accessor: provided bar () {} } ``` - Bare property names were required - Method & accessor ClassElements were provided - Not easily extensible to additional constraints - Not easily extensible to provided data properties
### Now ```js protocol P { // Explicit marker for required members requires foo; // Everything else is provided bar () {} baz = 0 } ``` - `requires` context-dependent keyword - TBD: `requires`? `abstract`? something else? - Methods and accessors are provided - Data properties can also be provided
## ComputedPropertyName for literal member names
### Before ```js protocol P { // Added as P.foo foo () {} // Added as a literal "bar" property "bar" () {} } ``` - Bare property names generate symbols - Quoted strings generate literal strings - No way to use external symbols (e.g. `Symbol.iterator`)
### Now ```js protocol P { // Still added as P.foo foo () {} // Added as a literal "bar" property ["bar"] () {} // Look ma, external symbols! [Symbol.iterator] () {} } ```
## Framing around *objects*, not *constructors*
### Before ```js protocol P { requires static foo; bar () {} } class C implements P {} C implements P; // true (new C) implements P; // false! ``` - `class C implements P` = `Protocol.implement(C, P)` - Direct protocol members added on `C.prototype`, explicit `static` members added directly on `C` - Result: cannot implement on plain objects! - Result: cannot check on instances!
### Now ```js protocol P { requires constructor implements protocol { foo() {} } bar () {} } class C implements P {} C.prototype implements P; // true (new C).bar(); // true ``` - No special syntax for constructors, all framed around objects - `Protocol.implement(obj, P)`: Direct protocol members added on `obj` - Sub-protocols for deeper requires/provides - Dropped `static` in favor of `constructor`/`prototype` sub-protocols - `class C implements P` is now sugar for `Protocol.implement(C.prototype, P)`
## Implementing a protocol - `Protocol.implement(obj, P)` - `class C implements P { /* ... */ }` (on `C.prototype`) - Dropped: Inline implementations for existing classes (`implemented by`) - Dropped: New ClassElement for declaring protocol implementation (`implements protocol P { /* ... */ }`)
## Protocol flattening ```js protocol P { requires a; b() {} } protocol Q { requires [P.b]; c() {} } class C implements P, Q { [P.a]() { /* ... */ } } ``` - How does variadic `implements` syntax work? - `C` should not need to also implement `P.b`! - _Flattening_ to the rescue! - Flattening: Union of all required and all provided members - Expose to authors via `Protocol.union()`
## Protocol introspection ```js protocol P { foo () { // elided } } ``` - Before: No way to access a protocol's members. - `P.foo` is a symbol. How to access the function? - How to disambiguate?? - Now: `Protocol.describe(P)` returns an object with the same syntax as the object passed to `new Protocol(...)` - Descriptor object shape TBD
## Immutability - Before: Undefined whether protocols are mutable or immutable - Now: Protocols are immutable - `Protocol.describe(P)` + `new Protocol(...)` can be used to create new protocols based on existing ones
## Reducing boilerplate ([#47](https://github.com/tc39/proposal-first-class-protocols/issues/47)) ```js 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 */ } // ... } ```
```js class MySlider implements FormAssociated { get [FormAssociated.formValue]() { return this.value; } set [FormAssociated.formValue](value) { this.value = value; } } ``` ```js class MySlider implements FormAssociated { get [FormAssociated.formValue]() { return this.value; } set [FormAssociated.formValue](value) { this.value = value; } 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]; } // ... } ```
- The protocol _could_ define all provided members as strings, but that moves control from consumer to producer - What if it were a property of the _implementation_ (i.e. the protocol → object relationship)? { .full-width .delayed }
## Auto-generated string aliases via factory ```js Protocol.implement(obj, Protocol.withStrings(P)); class C implements Protocol.withStrings(P) { /* ... */ } ``` - Name TBB (help?) - Takes a protocol and returns a new protocol that includes string accessors for all provided members - **Memoized**: same input protocol → same output protocol

Remaining Design Work

## Open questions - Are `"foo"` and `foo` distinct members? - Should `constructor` and `prototype` be automatically strings? - Inheritance: `extends`, `implements`, both? - How are parent symbols accessed? - How are sub-protocol symbols accessed?
## Are `"foo"` and `foo` distinct members? ([#59](https://github.com/tc39/proposal-first-class-protocols/issues/59)) ```js protocol P { foo() {} ["foo"]() {} } Protocol.describe(P); // => { // foo: { value: function } // } ``` - Object shape needs to work as both `Protocol.describe(...)` output and `new Protocol(...)` input - `foo` and `["foo"]` are technically distinct: `foo` creates a `P.foo` symbol, `["foo"]` is a literal string - But that complicates the object literal shape - If we disallow both on the same protocol, we can use a simple descriptor shape
## Should `constructor` and `prototype` be automatically strings? ([#84](https://github.com/tc39/proposal-first-class-protocols/issues/84)) - We don't want `P.constructor` and `P.prototype` symbols! - Frequently needed: now the only way to provide/require static members - Why create an error condition when we can just handle it? - But would that complicate the mental model too much?
## Protocol inheritance - `extends`, `implements`, both? - If `extends`, does it use the prototype chain? - If `extends`, does it support one or multiple parent protocols? - If multiple, inconsistent `extends` in constructors - If one, limited utility
## How are parent symbols accessed? ([#23](https://github.com/tc39/proposal-first-class-protocols/issues/23))
```js protocol P { requires foo() {} } ``` ```js protocol Q extends P { // Or Q implements P requires bar() {} } ``` ```js class C implements Q { [Q.bar]() {} // [Q.foo] or only [P.foo]? } ```
## How to access sub-protocol members? ([#81](https://github.com/tc39/proposal-first-class-protocols/issues/81))
```js protocol P { requires foo implements protocol { requires bar; } } ``` **Problem:** How to refer to `bar` from the implementing object?
- `Protocol.describe(Protocol.describe(P).members.foo).members.bar` is *extremely* unwieldy. 🤢 - `P.bar`? Conflicts with `P`’s direct members - `P[P.foo].bar`? - `P.foo.bar`?