Constrained Mixins
In this page:
Why Constrain a Mixin?
A mixin sometimes needs a specific property or method already present on the base class before it can add anything useful — a structural constraint makes that requirement explicit and checkable by TypeScript.
Example: Why Constrain a Mixin?
interface HasName { name: string; }
type Constructor<T = {}> = new (...args: any[]) => T;
function Greetable<TBase extends Constructor<HasName>>(Base: TBase) {
return class extends Base {
greet() { return `Hello, ${this.name}`; }
};
}
class Person { name = "Ravi"; }
class GreetablePerson extends Greetable(Person) {}
console.log(new GreetablePerson().greet());
Constraining with Interfaces
An interface can describe exactly what capability the base class must already have; the mixin's constructor-type parameter then combines that interface with a construct signature to enforce the requirement.
Example: Constraining with Interfaces
interface Timestamped { createdAt: Date; }
type Constructor<T = {}> = new (...args: any[]) => T;
function Auditable<TBase extends Constructor<Timestamped>>(Base: TBase) {
return class extends Base {
age() { return Date.now() - this.createdAt.getTime(); }
};
}
class Record_ { createdAt = new Date(); }
class AuditableRecord extends Auditable(Record_) {}
console.log(typeof new AuditableRecord().age());
Constrained Method Requirements
A mixin can require a specific method instead of a field — the constraint ensures the base class actually exposes a callable member matching the signature the mixin depends on internally.
Example: Constrained Method Requirements
interface Serializable { serialize(): string; }
type Constructor<T = {}> = new (...args: any[]) => T;
function Loggable<TBase extends Constructor<Serializable>>(Base: TBase) {
return class extends Base {
log() { console.log(this.serialize()); }
};
}
class Data {
serialize() { return "data payload"; }
}
class LoggableData extends Loggable(Data) {}
new LoggableData().log();
Multiple Constraints
When a mixin depends on several capabilities at once, combine them into a single interface — the base class passed in must satisfy every member of that combined interface, not just some of them.
Example: Multiple Constraints
interface HasName { name: string; }
interface HasAge { age: number; }
type Constructor<T = {}> = new (...args: any[]) => T;
function Describable<TBase extends Constructor<HasName & HasAge>>(Base: TBase) {
return class extends Base {
describe() { return `${this.name}, age ${this.age}`; }
};
}
class Person { name = "Ravi"; age = 25; }
class DescribablePerson extends Describable(Person) {}
console.log(new DescribablePerson().describe());
Benefits of Constrained Mixins
Constraints improve reliability because an incompatible base class gets rejected at compile time rather than failing mysteriously at runtime, and they double as documentation of exactly what the mixin expects from its input.
Example: Benefits of Constrained Mixins
interface HasId { id: number; }
type Constructor<T = {}> = new (...args: any[]) => T;
function Findable<TBase extends Constructor<HasId>>(Base: TBase) {
return class extends Base {
key() { return `record-${this.id}`; }
};
}
class Entity { id = 1; }
class FindableEntity extends Findable(Entity) {}
console.log(new FindableEntity().key());
Chapter Quiz — Complete all 4 topics to unlock
0/4 topics done
Complete these topics first: