Learn by reading through in order

Composition vs. Inheritance — Object.assign and Mixins

Split features into pieces and combine them in one object. Covers Object.assign, functions that return pieces, mixin functions, and when to use inheritance instead.

Your company's document system needs PDF export for meeting minutes, approval for contracts, and both for quotes. A class can name only one parent after extends, so you end up writing a class for every combination of features.

This article covers composition, which splits features into separate pieces and adds them to objects, and mixin functions, which stack features onto a class.

Adding Only the Pieces Each Document Needs — Object.assign

Suppose you've built an ApprovableDocument class that supports approval, and then, since quotes also need PDF export, an ExportableDocument class on top of it. Meeting minutes only need PDF export, so they'd either inherit approval they never use, or extend Document separately and copy the PDF code over.

With composition (building an object from separate groups of methods, one group per feature), you pass the pieces to Object.assign(target, piece1, piece2). It copies the properties of every argument after the first onto the first one, and returns that first object.

class Document {
  constructor(title) { this.title = title; }
}

// Make one group of methods (a piece) per feature
const approvable = {
  approve(name) { return `${this.title}: approved by ${name}`; },
};
const exportable = {
  toPdfName() { return `${this.title}.pdf`; },
};

// Copy in only the pieces each document needs
const minutes = Object.assign(new Document("Weekly meeting minutes"), exportable);
const estimate = Object.assign(new Document("Repair quote"), approvable, exportable);

console.log(Object.keys(estimate).join(", "));   // title, approve, toPdfName
console.log(estimate.approve("Alice"));         // Repair quote: approved by Alice
console.log(minutes.toPdfName());               // Weekly meeting minutes.pdf
console.log(typeof minutes.approve);            // undefined (the minutes have no approval)
The Order in Which Object.assign Copies
new Document("Repair quote")At this point itsonly key is titleCopies approvefrom approvableapprovable itselfdoesn't changeCopies toPdfNamefrom exportableReturns that sametarget instance
Each piece's properties are copied onto the same instance, from left to right. No new object is created; the return value is the target itself.

In estimate.approve("Alice"), estimate is left of the dot, so this inside approve is estimate, and this.title is the quote's title. The target is still an instance of new Document, so instanceof Document is true as well.

In a team chat app, you'll add a pin feature to incoming messages. messages and pinnable are already declared.

① Copy pinnable into the first message and store the return value in a variable.

② Pin the message through the variable from ①, then print the return value and the first message's pinned property.

③ Print whether the return value from ① is the same object as the first message.

④ Print the type of the second message's pin.

(If it runs correctly, an explanation will appear.)

JavaScript / TypeScript Editor

Run code to see output

Giving Each Document Its Own Comments — Functions That Return Pieces

Now suppose you also want a comment list, so you make { comments: [], addComment(text) { ... } } a piece and copy it into both the minutes and the quote with Object.assign. Only a reference to the array is copied, so both documents' comments point to the same array, and a comment added to the minutes also shows up in the quote's list.

If you rewrite the comment piece as commentable, a function that creates and returns a new piece each time you call it, the [] for comments is a different array on every call. Make approvable a function too and write { ...approvable(), ...commentable() }: spread syntax copies each piece's keys and values into a new object.

// A function that returns a piece: every call creates a new object and array
const commentable = () => ({
  comments: [],
  addComment(text) { this.comments.push(text); },
});
const approvable = () => ({
  approvedBy: "pending",
  approve(name) { this.approvedBy = name; },
});

// Spread the pieces to create a new document
const minutes = { title: "Weekly meeting minutes", ...commentable() };
const estimate = { title: "Repair quote", ...approvable(), ...commentable() };

minutes.addComment("Next: the 10th");
estimate.approve("Alice");
console.log(minutes.comments.length, estimate.comments.length);   // 1 0 (separate arrays)
console.log(estimate.approvedBy);                                  // Alice
console.log(Object.keys(estimate).join(", "));   // title, approvedBy, approve, comments, addComment
Do Comments on the Minutes Show Up on the Quote?
Object.assign thesame piece to bothcomments refers tothe same arrayminutes.addComment("Next: the 10th")estimate.commentshas 1 tooCall commentable()per documentcomments is a new[] on every callminutes.addComment("Next: the 10th")estimate.commentshas 0
After one comment is added to the minutes, it also shows up on the quote, but only in the top row. What gets copied is a reference to the array, so create the piece again for each document.

The { } is what creates the new object; spread syntax just copies the keys of each piece's return value into it. Unlike Object.assign, you don't prepare a target first, so what you get is a plain object, not an instance of a class.

With Duplicate Keys, the Last Piece Wins

With both Object.assign and spread syntax, when two pieces share a key name, the value from the piece passed later wins. If approvable() and commentable() both had a describe, only the later one would remain, without any error, so keep method names unique across pieces.

In a team chat app, you'll create messages that have reactions and read receipts. reactable is already declared.

① Define a function that returns a new piece on every call. The piece adds the name of each person who read the message to an array. Add each name only once.

② Combine the two pieces to create a message from Alice and one from Bob.

③ Add a reaction to Alice's message and mark it as read three times, with one person reading it twice.

④ Print the number of reactions and readers for both messages.

JavaScript / TypeScript Editor

Run code to see output

Stacking Features in a Class Definition — Mixin Functions

Elsewhere in the system, quotes are defined as class Estimate and created with new Estimate(...) on each screen. If you add approval and comments afterward with Object.assign, you have to copy them in after every new, and any quote you miss throws a TypeError when approve is called.

A mixin function (a function that takes a class and returns a class that extends it with extra features) is written as (Base) => class extends Base { ... }. class extends Base { ... } is a class expression with no name. You can write a call to this function after extends, and the class it returns becomes the parent.

class Document {
  constructor(title) { this.title = title; }
  describe() { return this.title; }
}

// Takes a class and returns a class that extends it with extra features
const Approvable = (Base) => class extends Base {
  approve(name) { this.approvedBy = name; }
  describe() { return `${super.describe()} (${this.approvedBy ?? "pending"})`; }
};
const Commentable = (Base) => class extends Base {
  comments = [];                                  // A new array for each instance
  addComment(text) { this.comments.push(text); }
};

// Both are stacked in the class definition, so there's nothing to copy in after each new
class Estimate extends Commentable(Approvable(Document)) {}
const estimate = new Estimate("Repair quote");
estimate.approve("Alice");
console.log(estimate.describe());   // Repair quote (Alice)
The Two Classes Stacked Above Estimate
class Estimatewith an empty bodyLookup of describestarts hereClass returnedby Commentablecomments andaddCommentClass returnedby Approvableapprove, describefound hereclass Documentsuper.describe()→ "Repair quote"
The inner Approvable sits closer to Document. Even though Estimate is empty, method lookup goes through all four classes in order.

In Approvable, super.describe() calls the describe of Document, the Base it received. Even if you swap the stacking order to Approvable(Commentable(Document)), you still get both approval and comments.

Extending a Mixin Without Calling It Throws a TypeError

extends Approvable makes the arrow function itself the parent. An arrow function can't be a class's parent, so you get TypeError: Class extends value ... is not a constructor or null. Write Approvable(Document) as the parent instead.

Comparing the Two Approaches — Mixins vs. extends

As you write a mixin function for each feature, the parentheses start to nest, as in class Estimate extends Commentable(Approvable(Document)) {}. The more you stack, the harder it gets to tell from that one declaration line what Estimate is a kind of and which features were added on top.

Compare it with an inheritance version, where CommentableDocument (comments) extends ApprovableDocument (approval). With either approach, instanceof Document is true and you can use super. The difference is whether you can pick the features for each class.

class Document { constructor(title) { this.title = title; } }
const Commentable = (Base) => class extends Base {   // The same mixin as in the previous section
  comments = []; addComment(text) { this.comments.push(text); }
};

// Inheritance: build the comment class on top of the approval class
class ApprovableDocument extends Document {
  approve(name) { this.approvedBy = name; }
}
class CommentableDocument extends ApprovableDocument {
  comments = [];
  addComment(text) { this.comments.push(text); }
}
const oldMinutes = new CommentableDocument("Weekly meeting minutes");
console.log(typeof oldMinutes.approve);    // function (it inherits approval it never uses)

// Stacking with a mixin
class Minutes extends Commentable(Document) {}
const minutes = new Minutes("Weekly meeting minutes");
console.log(typeof minutes.approve);       // undefined (only the comment features are added)
console.log(minutes instanceof Document, oldMinutes instanceof Document);   // true true
Features the Minutes Get with Inheritance vs. Composition
oldMinutes is aCommentableDocumentIts parent isApprovableDocumentapprove found inApprovableDocumenttypeof approveis functionminutes isa MinutesIts parent is theCommentable classNo approve all theway up to Documenttypeof approveis undefined
With inheritance, the minutes even get approve from the parent ApprovableDocument. With the mixin, they get only Commentable's features.

Put features that some kinds of objects need and others don't, like approval and comments, into pieces, and use extends for relationships that fit a single parent-child line, such as "a quote is a kind of document." The table below lists the four approaches used in this article.

ApproachHow features are addedGood for
extends ParentInherits everything from the parentAn "is a kind of" relation
Object.assign(target, piece)Copies into an existing objectExtending one object you got
{ ...piece() }Copies into a new objectBuilding without a class
extends Mixin(Parent)Stacks chosen featuresAdding in a class definition

In a team chat app, you'll create three kinds of messages: pin only, reactions only, and both. Message and Reactable are already declared.

① Define a mixin function that returns a class with pinning added.

② Define the three classes.

③ Create one message of each kind and print the types of pin and react.

④ Add a reaction to the message that has both, then print the pin result and the reaction count.

JavaScript / TypeScript Editor

Run code to see output
QUIZ

Knowledge Check

Answer each question one by one.

Q1What does instanceof Document return for minutes, which had pieces copied into it with Object.assign?

Q2If you copy one piece's comments: [] into two documents with Object.assign and push to one of them, what happens?

Q3In Commentable(Approvable(Document)), which class directly extends Document?