01 / The idea
Copying the save methods is a fair first answer.
You’re building a conference guide. Articles and sessions are different things: a session
has a speaker and a time slot, an article has neither. Readers still expect to save both for
later. The first version gives each type the same three methods: bookmark, removeBookmark, and isBookmarked.
Read the first versionTypeScript · the code this lesson starts from
// The first version duplicates the same behavior in each unrelated type.
export class PlainArticle {
private bookmarked = false;
readonly title: string;
constructor(title: string) {
this.title = title;
}
bookmark(): void {
this.bookmarked = true;
}
removeBookmark(): void {
this.bookmarked = false;
}
isBookmarked(): boolean {
return this.bookmarked;
}
}
export class PlainSession {
private bookmarked = false;
readonly title: string;
readonly speaker: string;
constructor(title: string, speaker: string) {
this.title = title;
this.speaker = speaker;
}
bookmark(): void {
this.bookmarked = true;
}
removeBookmark(): void {
this.bookmarked = false;
}
isBookmarked(): boolean {
return this.bookmarked;
}
} Go’s first version repeats the same methods on two structs. Both work. The pressure comes later: every new host and every change to the rule means another copy to write and keep in step.
Then speaker profiles need a save button too. Making Speaker extend Article would claim a relationship that isn’t true, and a third copy of the methods
is one more place for the rule to drift.
A mixin is a reusable unit of behavior that is attached to a host type without making that host a subtype of another domain type. In TypeScript, a mixin is usually a function that takes a class and returns a subclass with the capability added. Go has no mixin syntax; embedding a behavior value is the closest native shape.
That is what the DOM does with ParentNode. Document, DocumentFragment, and Element share no parent that owns append or querySelector. Each type includes the mixin, and the
methods arrive written once. Section 05 follows the idea into your components, where it
looks different.
02 / See the shape
Write the capability once. Attach it to any host.
Basic form is the whole mechanism: a function that returns a class with
private bookmark state and a label that reads the host’s own title. Switch to In the wild for Article and Session with the capability attached, and At the call site for both saved through one small Bookmarkable contract, plus a second capability stacked on top.
Both languages print the same result. Each says it in its own way: TypeScript returns a new class, and Go embeds a value whose methods are promoted to the host.
A class mixin adds bookmark state and methods to any host constructor, while Go embeds a reusable behavior value.
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- a mixin base must accept any constructor
type Constructor<T = object> = new (...args: any[]) => T;
// A mixin returns a class that adds one coherent capability to any compatible host.
// The host must have a title, because the capability reads it through `this`.
export function withBookmarking<TBase extends Constructor<{ title: string }>>(Base: TBase) {
return class extends Base implements Bookmarkable {
#bookmarked = false;
// A held helper object could not see the host; the mixin's `this` is the host.
bookmarkLabel(): string {
return this.#bookmarked ? `Saved: ${this.title}` : this.title;
}
bookmark(): void {
this.#bookmarked = true;
}
removeBookmark(): void {
this.#bookmarked = false;
}
isBookmarked(): boolean {
return this.#bookmarked;
}
};
}
// A second mixin shows that capabilities can be stacked when their state and names are separate.
export function withHighlighting<TBase extends Constructor>(Base: TBase) {
return class extends Base {
#highlighted = false;
highlight(): void {
this.#highlighted = true;
}
isHighlighted(): boolean {
return this.#highlighted;
}
};
} // Bookmarkable is the capability any host gains by embedding BookmarkState.
type Bookmarkable interface {
Bookmark()
RemoveBookmark()
IsBookmarked() bool
}
// BookmarkState is a reusable behavior value. Embedding promotes its methods to the host.
type BookmarkState struct {
bookmarked bool
}
func (state *BookmarkState) Bookmark() { state.bookmarked = true }
func (state *BookmarkState) RemoveBookmark() { state.bookmarked = false }
func (state *BookmarkState) IsBookmarked() bool { return state.bookmarked }
// An embedded value cannot see the struct that embeds it, so the host passes its title in.
func (state *BookmarkState) BookmarkLabel(title string) string {
if state.bookmarked {
return "Saved: " + title
}
return title
}
type HighlightState struct {
highlighted bool
}
func (state *HighlightState) Highlight() { state.highlighted = true }
func (state *HighlightState) IsHighlighted() bool { return state.highlighted } Reading the TypeScriptA function that returns a class
withBookmarking(Base) returns a new class that extends the host class you
pass in. Its #bookmarked field belongs to each object made from that class, not
to the function and not to every object on the page.
bookmarkLabel reads this.title. Inside the mixin, this is the host, so the capability can use the host’s members. A helper object
held in a field couldn’t, unless the host passed them in.
Session has a label() of its own, with the speaker in it. The
capability’s method is called bookmarkLabel so the two never meet. Section 04
shows what happens when they do.
Reading the GoEmbedding is the closest native form
Go embeds BookmarkState in both structs. Its methods are promoted, so
callers can write article.Bookmark(). The structs stay different, and the
unexported boolean stays inside the behavior value. SaveForLater accepts
the named Bookmarkable interface.
An embedded value can’t see the struct around it, so BookmarkLabel takes
the title as an argument: article.BookmarkLabel(article.Title). That is the
line where Go’s embedding stops being a mixin and becomes a held helper.
This isn’t overriding. If promotion exposes methods the host shouldn’t offer, write explicit wrappers; the Delegation lesson covers that forwarding choice.
03 / Watch the capability
The behavior is shared. The state is not.
Five steps, all running the TypeScript you just read. The first uses the copied methods; the rest attach the capability. Before each step, guess whether sharing the behavior also shares the saved flag.
In Try it, switch between the two designs, pick a host, and save or unsave it.
Where should this behavior live?
Copy the behavior into each host. Hosts: PlainArticle, PlainSession. Capability: bookmark methods in Article, bookmark methods in Session. article.bookmark() → saved; session.bookmark() → saved; new host → needs another copy. The first version works, but the same capability has two implementations and a third host would add a third copy.
Two copies work.
The behavior is correct, but every host owns a separate implementation.
Reduced motion: choose a scene to see its completed state.
Read this scene
The behavior is correct, but every host owns a separate implementation.
Copy the behavior into each host. Hosts: PlainArticle, PlainSession. Capability: bookmark methods in Article, bookmark methods in Session. article.bookmark() → saved; session.bookmark() → saved; new host → needs another copy. The first version works, but the same capability has two implementations and a third host would add a third copy.
Watch restarts when you return. Try it starts with fresh Article and Session hosts.
What one capability buys you
Now put names on what you just watched. Each one points at something in the code above.
- One implementation, many hosts
withBookmarking(Article)andwithBookmarking(Session)share one body. The starting design had the same three methods twice.- No false subtype
- Neither host extends the other. Step 3 checks
article instanceof Session, and it isfalse. - State per instance
- Each object gets its own
#bookmarked. In step 4 the session is unsaved and the article stays saved. - A contract as small as the caller
saveForLater(item: Bookmarkable)needs three methods, not an Article or a Session. Go’sSaveForLatertakes the same interface.- Capabilities that stack
withHighlighting(BookmarkedArticle)adds its own state and names. Step 5 saves and highlights one article without either touching the other.
None of that is free. Section 08 is the other side: names, state, and lifetimes that the mixin does not settle for you.
04 / Try a decision
A shorter name quietly replaces the host’s method.
A teammate finds bookmarkLabel long-winded and renames it to label, in the TypeScript mixin and on Go’s BookmarkState. Article
cards still read “Saved: Opening keynote notes”, and the tests for articles pass. Then
someone looks at a session card.
05 / Give it a real job
The host keeps its identity. The capability handles one concern.
A guide card knows whether it shows an article or a session. The save button doesn’t need
either host’s full shape. It needs the three methods of Bookmarkable. The host
still owns its title, its speaker, and its own label.
Owns identity
Article and Session keep their unrelated data and methods.
Owns one capability
Bookmark state and operations live together and attach where they’re needed.
Uses the contract
A save control asks for Bookmarkable, never for a concrete host.
The example keeps the saved flag inside each object. A real guide would keep the reader’s saved list in one place, so a save survives the card being thrown away, and would persist and sync it. A mixin shares behavior; it doesn’t decide where the data lives.
Build UIs?Every element you render is built from mixins, and your components share behavior the way mixins meant to.
Where it already is in your components
Every element you render is a host with mixins attached. The append and querySelector you call on a ref come from ParentNode. The onclick property on a button comes from GlobalEventHandlers,
which the HTML standard includes in HTMLElement, Document, and Window. Three unrelated types, one set of handlers.
Your own components don’t use class mixins, and that is on purpose. React’s old createClass took a mixins array, and in 2016 the React team wrote them off for implicit dependencies, name clashes, and snowballing complexity. Svelte 5 components are
functions, so there is no class to mix into. The idea survived in another form: a custom hook
in React, or a function with runes in Svelte, adds one capability to whichever component calls
it, and each call gets its own state.
When you have to own it
Now it’s the guide itself. Article cards, session cards, and speaker cards all need a save
button. Write the capability once, as useBookmark(id) in React or bookmark(id) in Svelte. It needs an id, not the card’s shape.
Two things from this lesson come along. Where the state lives is yours to decide: the
saved list belongs to the reader, not to a card, so it lives once above every card and
survives a reload. And names are yours: the session card already has a toggle for opening its abstract. A class mixin with a toggle would replace it, as
section 04 showed. A hook returns values and the card names them, toggle: toggleSaved, so the clash is settled where you can see it.
An article card and a session card each call one bookmark capability. Neither extends the other, and each call keeps its own saved flag.
import { useState } from 'react';
type Article = { title: string };
type Session = { title: string; speaker: string };
// The capability, written once. A class mixin adds it to a host class; a hook
// adds it to whichever component calls it. Each call gets its own state.
function useBookmark() {
const [saved, setSaved] = useState(false);
return { saved, toggle: () => setSaved((value) => !value) };
}
// Two unrelated hosts. Neither extends the other, and neither copies the state.
export function ArticleCard({ article }: { article: Article }) {
const bookmark = useBookmark();
return (
<article>
<h3>{article.title}</h3>
<button type="button" aria-pressed={bookmark.saved} onClick={bookmark.toggle}>
{bookmark.saved ? 'Saved' : 'Save'}
</button>
</article>
);
}
export function SessionCard({ session }: { session: Session }) {
const bookmark = useBookmark();
return (
<article>
<h3>{session.title}</h3>
<p>{session.speaker}</p>
<button type="button" aria-pressed={bookmark.saved} onClick={bookmark.toggle}>
{bookmark.saved ? 'Saved' : 'Save'}
</button>
</article>
);
}
06 / Recognize it elsewhere
Mixins show up wherever unrelated types need one behavior.
Bookmarks are one example. Here are places you have probably met a mixin without calling it one.
| Where you’ve seen it | The hosts | What the capability adds |
|---|---|---|
The DOM’s ParentNode | Document, DocumentFragment, Element | append, children, querySelector |
HTML’s GlobalEventHandlers | HTMLElement, Document, Window | onclick, oninput, and the other handler properties |
Python’s ThreadingMixIn | ThreadingHTTPServer and other socket servers | A thread for each request |
A Go struct that embeds sync.Mutex | Any struct that needs a lock | Lock and Unlock, promoted to the struct |
A Sass @mixin | Any rule that @includes it | A block of declarations, such as a focus ring |
Repeated methods alone aren’t enough. A mixin earns its place when the behavior can be named without listing the host’s whole shape.
07 / Already in your toolbox
The platforms you use already ship mixins.
Three to look at. For each one, find the hosts and the one place the shared methods are written.
TypeScript · mixins
The handbook’s pattern is this lesson’s: a Constructor type and a function
that returns class extends Base. It also lists what the pattern can’t do,
such as mixins through decorators.
DOM · ParentNode
The standard declares interface mixin ParentNode, then Document includes ParentNode and the same for DocumentFragment and Element. The method list is written once.
Go · embedding
Effective Go embeds a *log.Logger in a Job, so job.Println works. It also states the clash rule: a shallower name hides a deeper
one, and a duplicate at the same depth is an error once you use it.
A useful counterexample: React’s mixinsAn ecosystem that walked away
React’s createClass accepted a mixins array. By 2016 the team
had seen enough: mixins read state the component never declared, two mixins defined the
same method, and each new requirement made them harder to follow. The post “Mixins Considered Harmful” names those three problems and recommends other ways to share code. Hooks later became the
usual one.
Every one of those problems is on this page: the capability reading the host’s title, the label clash in section 04, and a mixin that grows past
one concern.
08 / The parts to watch
A mixin removes the copies. Names and state are still yours.
The copied methods made a few decisions obvious, because each class said everything in plain sight. A mixin moves them somewhere else.
Private state is per instance, not per mixin
The class returned by withBookmarking is reused, but every object gets its own
private field. If saved state should be shared across hosts, or outlive them, that is a separate
store and an ownership decision. The React and Svelte cards in section 05 make it.
The host may have a method already
A TypeScript mixin extends its host, so its method replaces a host method with the same
name, silently. In Go, a method declared on the host hides the promoted one. Section 04’s
rename compiles in both and gives opposite answers. Name the capability’s methods after
the capability, as bookmarkLabel is.
Two mixins can claim the same method
If withBookmarking and withHighlighting both defined reset, the one applied last would win in TypeScript, without a warning. In
Go, two embedded values with the same method at the same depth make the call ambiguous,
and the compiler refuses combined.Reset(). Use distinct names, or give the
host its own reset that calls both.
Go promotion can widen the surface
Embedding promotes every exported method of the embedded value. That is convenient when the whole small capability should be public. If only part of it should, write explicit methods on the host instead.
Construction and lifecycle remain yours
A mixin doesn’t decide when an object is created, disposed, persisted, or synced. A capability that subscribes to something still needs someone to clean it up.
09 / Make the call
What would you have to change tomorrow?
Give both designs a plausible change and follow the work it creates.
| The change | Copied methods in each host | One bookmark mixin |
|---|---|---|
| Speaker profiles need a save button | Write a third copy. | Attach withBookmarking to Speaker. |
| Saving should record when it happened | Edit every copy, and hope none is missed. | Edit the mixin once. Every host changes. |
| Sessions should unsave themselves once they end | Change PlainSession. No other host moves. | Add a flag or an override to the shared capability, or stop sharing it for sessions. |
| A capability method wants the host’s name | Each class picks its names in plain sight. | TypeScript lets the mixin win silently. Name by capability. |
| A reviewer asks what a session can do | Read one class. | Read Session and every mixin applied to it. |
Reach for a mixin when one behavior belongs on several hosts that share no parent, and should mean the same thing on each. The speaker profile is the moment: a third host, and the same rule.
Keep the copies when the hosts want different rules. Two classes that each say what they do are easier to read than a shared capability full of flags.
The question I’d leave beside the code is: what exactly is shared here, the host’s identity or one behavior with its own names and state?
10 / Take the idea with you
Explain the save button without saying “mixin.”
“Articles and sessions stay different types. Each gets the same save behavior from one place, with its own saved flag, and the save button only asks for the three bookmark methods.” That tells a reviewer more than the name does. When the reviewer wants the word, it’s cross-cutting concern: a behavior that runs across types that share no parent.
Before moving on, jot down why Session shouldn’t extend Article,
which language lets the host win a name clash, and one behavior in your own code that three
unrelated types copy. A save, select, or retry button counts.
Connections to follow nextRelated lessons
- Composition over inheritance builds routes from a list of parts. A mixin is a single part that attaches through the class itself.
- Delegation holds a helper and forwards to
it. That is where Go’s
BookmarkLabel(title)ends up. - Polymorphism is one interface with many implementations. A mixin is one implementation on many hosts.
- Extension adds methods to a type from outside it. A mixin adds them by making a new class.
- Decorator wraps one object to add behavior. A mixin adds it to the class, so every instance has it.