Mixins
Mixins are a pattern for reusing code across multiple classes without using traditional inheritance. TypeScript provides excellent support for type-safe mixins, allowing you to compose behaviors from multiple sources.
What are Mixins?
Mixins allow you to combine multiple classes into one, incorporating methods and properties from each. This is particularly useful when:
You need to share functionality across unrelated classes
You want to avoid deep inheritance hierarchies
You need multiple inheritance-like behavior
You’re implementing cross-cutting concerns
Mixins are a form of composition over inheritance, promoting more flexible and maintainable code.
Basic Mixin Pattern
The fundamental mixin pattern in TypeScript uses a function that takes a base class and returns a new class that extends it.
Simple Mixin Example
// Base class
class Animal {
name : string ;
constructor ( name : string ) {
this . name = name ;
}
}
// Mixin function
function Flyable < TBase extends new ( ... args : any []) => any >( Base : TBase ) {
return class extends Base {
fly () {
console . log ( ` ${ this . name } is flying` );
}
altitude : number = 0 ;
setAltitude ( altitude : number ) {
this . altitude = altitude ;
console . log ( ` ${ this . name } is now at ${ altitude } meters` );
}
};
}
// Applying the mixin
class Bird extends Flyable ( Animal ) {
constructor ( name : string ) {
super ( name );
}
}
const eagle = new Bird ( "Eagle" );
eagle . fly (); // "Eagle is flying"
eagle . setAltitude ( 1000 ); // "Eagle is now at 1000 meters"
Type-Safe Constructor Constraint
The constraint TBase extends new (...args: any[]) => any ensures:
TBase is a constructor function
It can be called with new
It returns any type of object
The mixin can extend it safely
Real-World Mixin Implementation
Here’s a practical example from the TypeScript source code implementing mixins for test utilities:
type Constructor < T = {}> = new ( ... args : any []) => T ;
// Mixin function with proper typing
function mixin < T extends Constructor >(
base : T ,
... mixins : (( klass : T ) => T )[]
) {
return mixins . reduce (( c , m ) => m ( c ), base );
}
// Timeout mixin
function Timeout < T extends Constructor >( base : T ) {
return class extends base {
timeout ( ms : number ) {
console . log ( `Setting timeout to ${ ms } ms` );
return this ;
}
};
}
// Clone mixin
function Clone < T extends Constructor >( base : T ) {
return class extends base {
clone () {
return Object . assign ( Object . create ( this ), this );
}
};
}
// Base class
class Task {
name : string ;
constructor ( name : string ) {
this . name = name ;
}
run () {
console . log ( `Running task: ${ this . name } ` );
}
}
// Apply multiple mixins
class EnhancedTask extends mixin ( Task , Timeout , Clone ) {
constructor ( name : string ) {
super ( name );
}
}
const task = new EnhancedTask ( "Build" );
task . timeout ( 5000 );
const clonedTask = task . clone ();
Advanced Mixin Patterns
Mixin with State
type Constructor < T = {}> = new ( ... args : any []) => T ;
// Timestamped mixin - adds creation and update timestamps
function Timestamped < TBase extends Constructor >( Base : TBase ) {
return class extends Base {
createdAt : Date = new Date ();
updatedAt : Date = new Date ();
update () {
this . updatedAt = new Date ();
}
getAge () : number {
return Date . now () - this . createdAt . getTime ();
}
};
}
// Activatable mixin - adds activation state
function Activatable < TBase extends Constructor >( Base : TBase ) {
return class extends Base {
private isActive : boolean = false ;
activate () {
this . isActive = true ;
console . log ( "Activated" );
}
deactivate () {
this . isActive = false ;
console . log ( "Deactivated" );
}
getStatus () : string {
return this . isActive ? "active" : "inactive" ;
}
};
}
// Using multiple mixins
class User {
constructor ( public username : string ) {}
}
class TrackedUser extends Timestamped ( Activatable ( User )) {
constructor ( username : string ) {
super ( username );
}
}
const user = new TrackedUser ( "john_doe" );
user . activate ();
console . log ( user . getAge ()); // Time since creation
console . log ( user . getStatus ()); // "active"
Mixin with Accessor Properties
Based on TypeScript’s conformance tests for mixin accessors:
type Constructor < T = {}> = new ( ... args : any []) => T ;
function Validated < TBase extends Constructor >( Base : TBase ) {
return class extends Base {
private _validationTarget : HTMLElement | null = null ;
get validationTarget () : HTMLElement {
if ( ! this . _validationTarget ) {
this . _validationTarget = document . createElement ( "input" );
}
return this . _validationTarget ;
}
validate () : boolean {
const element = this . validationTarget ;
// Perform validation logic
return element . checkValidity ();
}
};
}
class FormField {
constructor ( public name : string ) {}
}
class ValidatedField extends Validated ( FormField ) {
constructor ( name : string ) {
super ( name );
}
// Can override the accessor
get validationTarget () : HTMLElement {
return document . createElement ( "select" );
}
}
const field = new ValidatedField ( "email" );
const isValid = field . validate ();
Conditional Mixins
type Constructor < T = {}> = new ( ... args : any []) => T ;
function Loggable < TBase extends Constructor >( Base : TBase , enable : boolean = true ) {
if ( ! enable ) {
return Base ; // Return unchanged if disabled
}
return class extends Base {
log ( message : string ) {
console . log ( `[ ${ new Date (). toISOString () } ] ${ message } ` );
}
logError ( error : Error ) {
console . error ( `[ERROR] ${ error . message } ` );
}
};
}
class Service {
name : string ;
constructor ( name : string ) {
this . name = name ;
}
}
// Conditionally apply logging
const isDevelopment = process . env . NODE_ENV === "development" ;
class ProductionService extends Loggable ( Service , isDevelopment ) {
constructor () {
super ( "ProductionService" );
}
}
Mixin Factory Pattern
Create reusable mixin factories for common patterns:
type Constructor < T = {}> = new ( ... args : any []) => T ;
// Generic disposable pattern
function Disposable < TBase extends Constructor >( Base : TBase ) {
return class extends Base {
private disposed : boolean = false ;
private resources : (() => void )[] = [];
protected addResource ( cleanup : () => void ) {
this . resources . push ( cleanup );
}
dispose () {
if ( this . disposed ) return ;
this . resources . forEach ( cleanup => cleanup ());
this . resources = [];
this . disposed = true ;
}
isDisposed () : boolean {
return this . disposed ;
}
};
}
// Generic event emitter pattern
function EventEmitter < TBase extends Constructor >( Base : TBase ) {
return class extends Base {
private listeners = new Map < string , Function []>();
on ( event : string , callback : Function ) {
if ( ! this . listeners . has ( event )) {
this . listeners . set ( event , []);
}
this . listeners . get ( event ) ! . push ( callback );
}
emit ( event : string , ... args : any []) {
const callbacks = this . listeners . get ( event ) || [];
callbacks . forEach ( callback => callback ( ... args ));
}
off ( event : string , callback : Function ) {
const callbacks = this . listeners . get ( event ) || [];
const index = callbacks . indexOf ( callback );
if ( index > - 1 ) {
callbacks . splice ( index , 1 );
}
}
};
}
// Combine multiple mixins
class Connection {
constructor ( public url : string ) {}
connect () {
console . log ( `Connecting to ${ this . url } ` );
}
}
class ManagedConnection extends EventEmitter ( Disposable ( Connection )) {
constructor ( url : string ) {
super ( url );
this . addResource (() => {
console . log ( "Cleaning up connection" );
});
}
connect () {
super . connect ();
this . emit ( "connected" );
}
}
const conn = new ManagedConnection ( "https://api.example.com" );
conn . on ( "connected" , () => console . log ( "Connection established" ));
conn . connect ();
conn . dispose ();
Type Inference with Mixins
TypeScript’s type system can infer the final type of mixed classes:
type Constructor < T = {}> = new ( ... args : any []) => T ;
// Utility type to get instance type from constructor
type InstanceType < T > = T extends new ( ... args : any []) => infer R ? R : any ;
function Serializable < TBase extends Constructor >( Base : TBase ) {
return class extends Base {
serialize () : string {
return JSON . stringify ( this );
}
static deserialize ( json : string ) : InstanceType < typeof Base > {
return JSON . parse ( json );
}
};
}
function Comparable < TBase extends Constructor >( Base : TBase ) {
return class extends Base {
equals ( other : this ) : boolean {
return JSON . stringify ( this ) === JSON . stringify ( other );
}
};
}
class Point {
constructor ( public x : number , public y : number ) {}
}
const MixedPoint = Comparable ( Serializable ( Point ));
type MixedPointType = InstanceType < typeof MixedPoint >;
const p1 = new MixedPoint ( 10 , 20 );
const json = p1 . serialize ();
const p2 = new MixedPoint ( 10 , 20 );
console . log ( p1 . equals ( p2 )); // true
Mixin Constraints
Sometimes you need to constrain what types a mixin can be applied to:
// Constraint: Base must have an 'id' property
interface HasId {
id : string | number ;
}
type HasIdConstructor = new ( ... args : any []) => HasId ;
function Auditable < TBase extends HasIdConstructor >( Base : TBase ) {
return class extends Base {
private auditLog : Array <{ action : string ; timestamp : Date }> = [];
logAction ( action : string ) {
this . auditLog . push ({
action: ` ${ action } (ID: ${ this . id } )` ,
timestamp: new Date (),
});
}
getAuditLog () {
return [ ... this . auditLog ];
}
};
}
// This works - has 'id' property
class Document {
constructor ( public id : string , public title : string ) {}
}
class AuditedDocument extends Auditable ( Document ) {}
const doc = new AuditedDocument ( "doc-123" , "My Document" );
doc . logAction ( "created" );
doc . logAction ( "updated" );
console . log ( doc . getAuditLog ());
// This would fail - no 'id' property
// class NoId {}
// class AuditedNoId extends Auditable(NoId) {} // Error!
Best Practices
Each mixin should provide a single, well-defined piece of functionality. Don’t create “god mixins” that do too much. // Good: Focused mixin
function Timestamped < T extends Constructor >( Base : T ) {
return class extends Base {
timestamp = Date . now ();
};
}
// Bad: Mixin doing too much
function Everything < T extends Constructor >( Base : T ) {
return class extends Base {
timestamp = Date . now ();
log () {}
validate () {}
serialize () {}
// ... too many responsibilities
};
}
Add constraints to ensure mixins are applied to appropriate base classes. // Good: Constrained mixin
interface Named {
name : string ;
}
function Greetable < T extends Constructor < Named >>( Base : T ) {
return class extends Base {
greet () {
console . log ( `Hello, ${ this . name } ` );
}
};
}
Document Mixin Dependencies
Clearly document what properties or methods a mixin expects or provides. /**
* Adds validation functionality to a class.
*
* Requires:
* - Base class must have a 'validationTarget' property
*
* Provides:
* - validate(): boolean method
* - isValid(): boolean method
*/
function Validatable < T extends Constructor >( Base : T ) {
// implementation
}
Be careful when combining mixins that might have conflicting properties or methods. // Potential conflict - both define 'dispose'
class A extends Disposable ( EventEmitter ( Base )) {
// If both mixins define dispose(), there will be a conflict
}
// Better: Use namespacing or prefixes
function EventEmitterMixin < T >( Base : T ) {
return class extends Base {
emitterDispose () { /* ... */ }
};
}
Mixins vs. Other Patterns
Mixins vs. Inheritance
Mixins vs. Composition
Mixins vs. Decorators
Mixins:
Horizontal composition
Multiple sources of behavior
More flexible
Inheritance:
Vertical hierarchy
Single parent class
Simpler mental model
Use mixins when you need to combine behaviors from multiple sources. Mixins:
Behavior added to class prototype
Shared through inheritance
Access to this context
Composition:
Behavior delegated to separate objects
More explicit dependencies
Easier to test in isolation
Use composition for loose coupling, mixins for shared behavior. Mixins:
Add properties and methods
Modify class structure
Applied at class definition time
Decorators:
Modify behavior of existing members
Meta-programming
More granular control
Mixins and decorators complement each other well.
Common Use Cases
Cross-Cutting Concerns Logging, monitoring, authentication across multiple classes
Behavior Composition Combining multiple behaviors without deep inheritance
Framework Extensions Adding functionality to framework base classes
Feature Flags Conditionally adding features based on configuration
Limitations and Considerations
Mixins have some limitations to be aware of:
No static type checking across mixins : TypeScript can’t always infer the combined type perfectly
Runtime overhead : Each mixin adds a layer to the prototype chain
Debugging complexity : Stack traces can be harder to follow
Name collisions : Multiple mixins might define the same property
Decorators Combine mixins with decorators for powerful patterns
Type Checker Understand type inference with mixins