Class Spread Syntax
November 19, 2025 · View on GitHub
Table of contents
Status
Champion(s): Lea Verou
Author(s): Lea Verou
Stage: 0
Overview
This proposal introduces body-order-sensitive composition for classes through a spread syntax inspired from object spread syntax. Mixin application is treated as a class element, applied in the order it appears, interleaved with method and field definitions.
import * as methods from "./methods.js";
class A {
foo() { console.log("foo"); }
static bar = "bar";
}
class C {
...A;
...methods;
}
console.log(B.bar); // "bar"
(new B).foo(); // "foo"
It is meant as a modularization and code reuse tool for the more tightly coupled use cases, rather than a more general solution to all class composition use cases.
Motivation
Currently, the only mechanisms in the language for reusing class API surface are
- Good ol' single inheritance
- Subclass factories to emulate multiple inheritance by piggybacking on single inheritance
- Good ol' low-level prototype frobnication. 😅
However, the inheritance chain is observable from the outside and thus, inheritance-based methods can feel heavyweight and/or backwards incompatible, especially for minor code reuse tasks not intrinsically related to the class identity.
Low-level prototype frobnication can be done transparently, but has awkward ergonomics (needs to be a separate step), and does not have access to all class features (e.g. class fields). Additionally, having to be done imperatively, after the class definition, means that it cannot be interleaved with other methods — if the host class author wants to override a method, that also needs to be done imperatively.
Static initialization blocks help a little with ergonomics, but are executed after any methods have been defined, so any methods they add cannot be overridden by the host class author:
class A {
foo() { return 1 }
static {
this.prototype.foo = function() { return 2 }
}
foo() { return 3 }
}
console.log(new A().foo()); // 2
As discussed in the comparison with decorators section below, when class decorators are primarily meant to modify classes and class members, rather than to compose classes from building blocks. While they can be used for this purpose, they simply provide better ergonomics over these two patterns, rather than a new mechanism to compose classes from building blocks.
In other areas of the language, the spread syntax can be used to compose an object from multiple other objects of the same or similar type. We have it for iterables:
const arr = [...arr1, ...arr2];
Function arguments:
console.log(...iterable);
And for objects:
const obj = { ...obj1, ...obj2 };
But we don't have it for classes.
This proposal introduces body-order-sensitive composition for classes: mixin application is treated as a class element, applied in the order it appears, interleaved with method and field definitions. Later elements (whether defined directly in the class or introduced via composition) override earlier ones.
Use cases
Class modularity
For large classes, it is impractical to maintain the entire class API in a single file. However, the ergonomics of modularization are not great, and require a lot of glue code. This is a simplified real example from the Prism v2 branch (full source):
import { highlightAll } from '../highlight-all.js';
import { highlightElement } from '../highlight-element.js';
import { highlight } from '../highlight.js';
import { tokenize } from '../tokenize/tokenize.js';
export default class Prism {
highlightAll (options = {}) {
return highlightAll.call(this, options);
}
highlightElement (element, options = {}) {
return highlightElement.call(this, element, options);
}
highlight (text, language, options = {}) {
return highlight.call(this, text, language, options);
}
tokenize (text, grammar) {
return tokenize.call(this, text, grammar);
}
}
With regular JS, one can avoid maintaining the method signatures in two places, by using ...args.
E.g. we could have done this instead which minimizes knowledge duplication:
export default class Prism {
highlightAll (...args) {
return highlightAll.call(this, ...args);
}
highlightElement (...args) {
return highlightElement.call(this, ...args);
}
highlight (...args) {
return highlight.call(this, ...args);
}
tokenize (...args) {
return tokenize.call(this, ...args);
}
}
However with typed variants of the language (e.g. TypeScript), there is no way to avoid duplicating the function signatures.
With a spread syntax, it could look like this:
import * as methods from "./methods.js";
export default class Prism {
...methods;
}
There are even more egregious examples of this need in the wild, like this Babel class.
Certain mixin use cases
Given that class spread essentially has macro semantics, it’s suitable for mixin use cases where the mixin identities applied to a given class do not need to be preserved or introspected, but the separation mainly exists for maintainability purposes.
In some ways it’s analogous to the difference between object spread (which does not preserve where each property came from) vs Object.create() (which does).
TODO: Add more concrete use cases.
Code sharing across procedural and OOP APIs
There are use cases where a module wants to provide both a procedural API and an OOP one. E.g. Color.js does this. The procedural API can be better suited to performance-critical tasks and is more tree-shakable, while still allowing the OOP API to offer better ergonomics by preserving state across method calls.
procedural.js:
export function foo(...args) {
let arg = this ?? args.shift();
/* elided */
}
export function bar() {
let arg = this ?? args.shift();
/* elided */
}
// ...
oop.js:
import * as methods from "./procedural.js";
export default class Class {
...methods;
}
Dynamically generating API surface
Note that in the previous case, every method had to include the following boilerplate to cater to being used as either a procedural function, or an instance method:
let arg = this ?? args.shift();
We could also write the functions as procedural and generate the boilerplate when adding to the class:
export default class Class {
...Object.fromEntries(Object.entries(methods).map(([key, value]) => [key, (...args) => value(this, this, ...args)]))
}
Facilitating API glue code for the delegation pattern
A common OOP pattern is to achieve composition via delegation (or forwarding), where the implementing class uses a separate object to abstract and reuse behavior.
In Web Components, this pattern is known as Controllers.
The native ElementInternals API is an example of this pattern.
Lit even has a Controller primitive to facilitate this pattern.
There is a lot to like in this pattern:
- Separate state and inheritance chain makes it very easy to reason about
- Can be added and removed at any time, even on individual instances
- Can have multiple controllers of the same type for a single class
- Delegate does not need to be built for this purpose. E.g. in many objects the delegate is simply another object (a DOM element in Web Components, a data structure, etc.)
However, a major problem is that adding API surface to the host class involves a lot of repetitive glue code.
To reuse the ElementInternals example, making a web component behave like a form control involves glue code like:
For example, making a custom element that is also a form control looks like this:
class MyElement extends HTMLElement {
// This tells ElementInternals that this element is form associated
static formAssociated = true;
constructor() {
super();
// Cannot use #internals because subclasses need access
this._internals = this.attachInternals();
this.addEventListener("input", () => {
this._internals.setFormValue(this.value);
});
}
// API glue code
get labels () { return this._internals.labels; }
get form () { return this._internals.form; }
get validity () { return this._internals.validity; }
get validationMessage () { return this._internals.validationMessage; }
willValidate (...args) { return this._internals.willValidate(...args); }
reportValidity(...args) { return this._internals.reportValidity(...args); }
checkValidity(...args) { return this._internals.checkValidity(...args); }
// it goes on, and on, and on…
}
With a programmatic way to add API surface, it could look like this:
const formProperties = [
'labels', 'form', 'validity', 'willValidate',
//...
];
class MyElement extends HTMLElement {
constructor() {
super();
// Cannot use #internals because subclasses need access
this._internals = this.attachInternals();
this.addEventListener("input", () => {
this._internals.setFormValue(this.value);
});
}
...formProperties.reduce((acc, prop) => Object.defineProperty(acc, prop, { get: () => this._internals[prop] }), {});
}
Limitations
This is not intended to cover every mixin/trait/multiple inheritance use case. It is a low-level feature, intended to make tightly certain coupled use cases easier to manage. Spreading arbitrary classes onto arbitrary classes will often produce unexpected results and/or errors.
A few limitations that make it unsuitable for some use cases:
- There is no way to introspect whether a given class has been spread onto another
- There is no way to compose functions (e.g. to add side effects to lifecycle hooks)
- There is no declarative mechanism to handle naming collisions gracefully (e.g. via renaming)
superremains lexically bound, which can be surprising- Referencing private fields will produce errors since they are not spread
For example:
class A {
#foo = 1;
get foo() { return this.#foo }
}
class B { ...A }
const b = new B();
console.log(b.foo);
// TypeError: Cannot read private member #foo from an object whose class did not declare it
Detailed design
Copy by descriptor rather than value
It is important for this to be able to specify fields, accessors, and methods. This means that while (like object spread) the semantics are largely those of assignment, unlike object spread, it is descriptors not values that are copied.
Handling spreading of both objects and constructor functions
To satisfy use cases it is important to be able to spread both objects and constructor functions, so the algorithm needs to be able to handle both cases with good DX.
When spreading a constructor function, semantics are straightforward: instance members become instance members, and static members become static members.
But how does class A { ...Partial } work when Partial is an object?
Most use cases need to specify instance members, but to support that (without wrapping it in a boilerplate {prototype: Partial}) we'd need to introduce "magic" like:
- If there is a
prototypeproperty, use it as the source of instance members, otherwise use the object itself OR - If the object is a function, then use its prototype as the source of instance members, otherwise use the object itself
It may be better to take the small DX hit of having to do class A { ...{ prototype: Partial } } in exchange for the predictability that instance methods are always copied from Partial.prototype, and static methods are always copied from Partial.
Providing fields when spreading plain objects
If something like class field introspection is adopted,
it would allow providing fields by spreading regular objects too, by simply providing a Symbol.fields/Symbol.publicFields property on the object.
Spreading a constructor function into another class definition
As described above, spreading a constructor function copies (descriptors for):
- Instance members
- Static members
- All public [[Fields]]
It does not copy:
- Any fields or members in [[PrivateElements]] (TBD, see discussion below)
- Static initialization blocks (but it does copy their side effects, since by then they have already executed)
- Decorators. Those have already been applied to the class by then.
It does not affect the class's [[SourceText]], which includes the spread syntax itself.
super remains lexically bound, akin to regular assignment (see discussion below).
Alternative model: syntactic expansion
An alternative model would be that of syntactic expansion, where we basically use [[SourceText]] as-is and simply remove everything that is not in ClassBody.
Pros:
superwould resolve dynamically,- Private fields would also be copied
- Static initialization blocks, decorators would be preserved rather than simply their side effects
Cons:
- References would not be preserved. Every member would be a brand new object, rather than a reference to the original.
- While this seems like it would match author intent more closely, there is no precedent in the language for such a model, and that is not how spread syntax works in any other area of the language.
Comparison
Other languages
See prior art for a discussion on current userland patterns and related features in other languages.
To my knowledge, no existing mainstream language provides this symmetric, body-order-based override behavior for class composition.
This is similar in spirit to Ruby’s include/prepend, where:
- mixin operations occur inside the class body,
- the order of these operations affects the method lookup chain, and
- class bodies are executed imperatively rather than being purely declarative.
However, Ruby’s precedence rules are asymmetric and not fully order-local:
- Methods defined on the class always override methods from included modules, regardless of where include appears in the body.
- Methods from prepended modules always take precedence over the class’s methods, again regardless of body position.
By contrast, this proposal defines composition in terms of source order of class elements, consistent with the way spread syntax works in other areas of the language.
More languages support mixins-as-macros (e.g. Common Lisp, Nim, D, and Rust — sort of), though in those cases the semantics are purely those of syntactic expansion.
Other proposals
See Class Composition: Existing proposals for a discussion on other proposals related to class composition.
Here we discuss them in the context of this proposal.
Maximally minimal mixins (Subclass factories)
As discussed in Motivation, subclass factories are a prevalent pattern for class composition, but affect the inheritance chain, which is not always desirable.
Decorators
Conceptually, decorators are meant as a way to modify a class or class member, rather than as a way to compose classes from other classes or objects. However, it is possible to use decorators in that way: one can define a class decorator that also takes one or more classes or objects as parameters and extends the class with them.
However:
- Decorators are not interleaved with other class elements, but are executed after all other class elements have been defined, so there is no elegant way to override a method or field defined by a decorator.
- The only ways to add new API surface to the existing class are either to (a) use
addInitializer()and basically do the same things as prototype fudging, with the same pros & cons OR (b) return a new subclass of the class being decorated, with the same pros & cons as subclass factories.
First-class Protocols
Protocols elegantly solve the problem of naming collisions by using symbols. The downside is that since most classes want to expose a string-based API, this involves a lot of glue code. However, since one goal of this proposal is to make it easier to generate API glue code for classes, it can be synergetically combined with the protocol proposal to improve ergonomics.
Class field introspection
The class field introspection proposal is a proposal for exposing a read-only data structure containing class fields.
It is complementary to this proposal: without it, class fields are spread by directly taking them from [[Fields]], whereas with it, it can be used to explain and polyfill class spread syntax.
Implementation
Spreading an object into another class definition could desugar to something like the following.
Using Symbol.fields to represent class fields, which can be internal magic if class field introspection is not adopted.
function extendClass (Base, Partial) {
if (!Base.prototype) {
return;
}
// Exclude name, length, constructor etc.
let exclude = Object.getOwnPropertyNames(Function.prototype);
copyDescriptors(Base.prototype, Partial.prototype, { exclude });
// Statics
copyDescriptors(Base, Partial, { exclude });
if (Partial[Symbol.publicFields]?.length > 0) {
// Fictional, since even if field introspection is adopted,
// it would be read-only at first
Base[Symbol.publicFields].push(...Partial[Symbol.publicFields]);
}
}
function copyDescriptors (target, obj, {exclude = []} = {}) {
let descriptors = Object.getOwnPropertyDescriptors(obj);
for (const key in descriptors) {
if (exclude.includes(key)) continue;
Object.defineProperty(target, key, descriptors[key]);
}
}
There is no way to copy or mutate [[Fields]] in userland, but assuming they were introspectable AND mutable, it could look like:
B[Symbol.publicFields].push(...A[Symbol.publicFields]);
Note that all of this can be applied to an existing class as well.
Perhaps there could be a helper method to facilitate this, e.g. Function.extend(Base, ...traits),
with the spread syntax desugaring to it.
Discussion / Q & A
Lexical vs dynamic super
It could be argued that ideally, super should resolve based on the new class hierarchy,
but given that super is lexically bound, that ship has likely sailed.
It's probably far simpler to have super resolve using the same semantics as assignment:
class A {
foo() { return "A"; }
}
class Trait extends A {
foo() { return super.foo() + " Trait"; }
}
class B extends A {
foo() { return "B"; }
}
class C extends B {
// Same as C.prototype.foo = Trait.prototype.foo
...Trait;
}
console.log(new C().foo()); // "A Trait"
In some ways, this is more predictable. This is not necessarily a footgun, it can still be useful for cases where the spread class shares some of the same inheritance chain.
When the Trait needs to reference the host class's superclass dynamically, rather than its own superclass, that can still be done with a little more work:
class Trait extends A {
foo() {
let thisSuper = Object.getPrototypeOf(this.constructor)?.prototype;
return thisSuper.foo.call(this) + " Trait";
}
}
class C extends B {
...Trait;
}
console.log(new C().foo()); // "B Trait"
That said, use cases that need dynamic superclasses might be better suited to subclass factories.
What should happen with private elements?
Ideally, private elements would be copied as well, and any references to them would resolve based on the host class's corresponding private element. These would be separate from the spread class's private elements: it would be just a shorthand for creating new private elements.
While at first it seems like that would violate encapsulation, it does not reveal anything that is not already revealed by simply accessing the class's [[SourceText]].
However, it seems like that may be harder to implement (unless the semantics are those of syntactic expansion, which seems unlikely). In that case, it would be an acceptable compromise to simply not copy private elements, and have any methods referring to them throw (as would happen with assignment).
Is it possible to introspect whether a given class has been spread onto another?
By design, no (except through a shared contract, e.g. a Symbol property).
References are preserved, but there is no way to disentangle whether that is the result of spreading or regular assignment.
Can built-in classes be spread?
Yes, per the definition of how spreading works, though that is of limited utility in most cases.