A TypeScript library that provides tolerable Mixin functionality with multiple inheritance support.
—
Specialized mixing support for generic classes using the decorator pattern. Due to TypeScript limitations with generic parameters in base class expressions, generic class mixing requires a different approach using the mix decorator combined with declaration merging.
A decorator version of the Mixin function specifically designed for mixing generic classes.
/**
* A decorator version of the Mixin function for mixing generic classes
* Used when generic parameters prevent using Mixin in extends clause
* @param ingredients - Array of class constructors to mix
* @returns Class decorator function
*/
function mix(...ingredients: Class[]): (decoratedClass: any) => any;Usage Pattern:
The mix decorator must be used in combination with declaration merging to provide proper typing:
import { mix } from "ts-mixer";
// Base generic classes to mix
class Foo<T> {
public fooMethod(input: T): T {
return input;
}
}
class Bar<T> {
public barMethod(input: T): T {
return input;
}
}
// Declaration merging: interface provides the typing
interface FooBar<T1, T2> extends Foo<T1>, Bar<T2> {}
// Decorator provides the runtime behavior
@mix(Foo, Bar)
class FooBar<T1, T2> {
public fooBarMethod(input1: T1, input2: T2) {
return [this.fooMethod(input1), this.barMethod(input2)];
}
}
// Usage
const instance = new FooBar<string, number>();
const result = instance.fooBarMethod("hello", 42);
// result: ["hello", 42]
// TypeScript knows fooMethod and barMethod are availableUnlike the regular Mixin function, mix requires explicit declaration merging:
// This interface declaration is REQUIRED
interface MixedClass<T1, T2> extends BaseClass1<T1>, BaseClass2<T2> {}
// The decorator provides runtime behavior but no typing
@mix(BaseClass1, BaseClass2)
class MixedClass<T1, T2> {
// Your additional methods here
}The mix decorator does not provide automatic type inference like Mixin. You must manually specify the interface that extends all mixed classes.
import { mix } from "ts-mixer";
class Storage<T> {
private items: T[] = [];
store(item: T): void {
this.items.push(item);
}
retrieve(): T[] {
return [...this.items];
}
}
class Validator<T> {
private rules: ((item: T) => boolean)[] = [];
addRule(rule: (item: T) => boolean): void {
this.rules.push(rule);
}
validate(item: T): boolean {
return this.rules.every(rule => rule(item));
}
}
class Logger<T> {
log(message: string, item?: T): void {
console.log(`[LOG]: ${message}`, item);
}
}
// Declaration merging for all three generic classes
interface ValidatedStorage<T> extends Storage<T>, Validator<T>, Logger<T> {}
@mix(Storage, Validator, Logger)
class ValidatedStorage<T> {
storeWithValidation(item: T): boolean {
this.log("Attempting to store item", item);
if (this.validate(item)) {
this.store(item);
this.log("Item stored successfully");
return true;
} else {
this.log("Item validation failed");
return false;
}
}
}
// Usage
const storage = new ValidatedStorage<number>();
storage.addRule(x => x > 0);
storage.addRule(x => x < 100);
storage.storeWithValidation(50); // true, logs success
storage.storeWithValidation(-5); // false, logs validation failure
const items = storage.retrieve(); // [50]import { mix } from "ts-mixer";
// Base generic class
class Container<T> {
protected items: T[] = [];
add(item: T): void {
this.items.push(item);
}
}
// Another generic class extending Container
class SortedContainer<T> extends Container<T> {
add(item: T): void {
super.add(item);
this.sort();
}
private sort(): void {
this.items.sort();
}
}
// Mixin class with different generic parameter
class Timestamped<U> {
private timestamps: Map<U, Date> = new Map();
setTimestamp(key: U, date: Date = new Date()): void {
this.timestamps.set(key, date);
}
getTimestamp(key: U): Date | undefined {
return this.timestamps.get(key);
}
}
// Mixing a class hierarchy with another generic class
interface TimestampedSortedContainer<T, U>
extends SortedContainer<T>, Timestamped<U> {}
@mix(SortedContainer, Timestamped)
class TimestampedSortedContainer<T, U> {
addWithTimestamp(item: T, key: U): void {
this.add(item);
this.setTimestamp(key, new Date());
}
}
// Usage
const container = new TimestampedSortedContainer<string, number>();
container.addWithTimestamp("hello", 1);
container.addWithTimestamp("world", 2);
const timestamp = container.getTimestamp(1);
console.log(timestamp); // Date objectGeneric class mixing exists because TypeScript doesn't allow generic parameters in base class expressions:
// This is IMPOSSIBLE in TypeScript
class Mixed<T> extends Mixin(Generic1<T>, Generic2<T>) {} // ❌ ErrorYou must always provide the interface declaration. Without it, TypeScript won't know about the mixed-in methods and properties:
// Missing interface - TypeScript won't know about mixed methods
@mix(ClassA, ClassB) // ❌ Properties from ClassA, ClassB not available
class BadExample<T> {}
// Correct approach
interface GoodExample<T> extends ClassA<T>, ClassB<T> {}
@mix(ClassA, ClassB) // ✅ Properties available via interface
class GoodExample<T> {}The order of classes in the mix decorator affects method override precedence, just like with regular Mixin.
The same constructor limitations apply as with regular mixing - constructors receive separate this contexts.
Install with Tessl CLI
npx tessl i tessl/npm-ts-mixer