A TypeScript library that provides tolerable Mixin functionality with multiple inheritance support.
—
Decorator preservation and inheritance system for mixed classes. The decorate function ensures that decorators are properly inherited when classes are mixed together, solving the problem where decorators from mixin classes are lost.
Wraps decorators to ensure they are inherited during the mixing process.
/**
* Wraps decorators to ensure they participate in mixin inheritance
* Supports class, property, and method decorators
* @param decorator - Any decorator (class, property, or method)
* @returns Wrapped decorator that will be inherited during mixing
*/
function decorate<T extends ClassDecorator | PropertyDecorator | MethodDecorator>(
decorator: T
): T;Usage Pattern:
Replace regular decorators with decorate(decorator) in classes that will be mixed:
import { Mixin, decorate } from "ts-mixer";
// Instead of @SomeDecorator()
// Use @decorate(SomeDecorator())
class Validatable {
@decorate(IsString()) // instead of @IsString()
name: string = "";
@decorate(IsNumber()) // instead of @IsNumber()
age: number = 0;
}Class decorators are inherited and applied to the mixed class:
import { Mixin, decorate } from "ts-mixer";
// Custom class decorator
function Entity(tableName: string) {
return function<T extends { new(...args: any[]): {} }>(constructor: T) {
(constructor as any).tableName = tableName;
return constructor;
};
}
@decorate(Entity("users"))
class User {
id: number = 0;
name: string = "";
}
@decorate(Entity("profiles"))
class Profile {
bio: string = "";
avatar: string = "";
}
class UserProfile extends Mixin(User, Profile) {
fullProfile(): string {
return `${this.name}: ${this.bio}`;
}
}
// Both decorators are applied to the mixed class
console.log((UserProfile as any).tableName); // "profiles" (last one wins)Property decorators are inherited for both instance and static properties:
import { Mixin, decorate } from "ts-mixer";
import { IsEmail, IsString, Length } from "class-validator";
class PersonalInfo {
@decorate(IsString())
@decorate(Length(2, 50))
firstName: string = "";
@decorate(IsString())
@decorate(Length(2, 50))
lastName: string = "";
}
class ContactInfo {
@decorate(IsEmail())
email: string = "";
@decorate(IsString())
phone: string = "";
}
class Person extends Mixin(PersonalInfo, ContactInfo) {
getFullName(): string {
return `${this.firstName} ${this.lastName}`;
}
}
// All property decorators are inherited
const person = new Person();
person.firstName = "John";
person.lastName = "Doe";
person.email = "john.doe@example.com";
// Validation decorators work on the mixed class
import { validate } from "class-validator";
validate(person).then(errors => {
console.log("Validation errors:", errors);
});Method decorators are inherited and applied to methods in the mixed class:
import { Mixin, decorate } from "ts-mixer";
// Custom method decorator for logging
function Log(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const originalMethod = descriptor.value;
descriptor.value = function(...args: any[]) {
console.log(`Calling ${propertyKey} with args:`, args);
const result = originalMethod.apply(this, args);
console.log(`${propertyKey} returned:`, result);
return result;
};
return descriptor;
}
class Calculator {
@decorate(Log)
add(a: number, b: number): number {
return a + b;
}
}
class StringUtils {
@decorate(Log)
reverse(str: string): string {
return str.split("").reverse().join("");
}
}
class MathStringUtils extends Mixin(Calculator, StringUtils) {
@decorate(Log)
addAndReverse(a: number, b: number): string {
const sum = this.add(a, b);
return this.reverse(sum.toString());
}
}
const utils = new MathStringUtils();
const result = utils.addAndReverse(12, 34); // Logs all method calls
// Calling add with args: [12, 34]
// add returned: 46
// Calling reverse with args: ["46"]
// reverse returned: "64"
// Calling addAndReverse with args: [12, 34]
// addAndReverse returned: "64"Real-world example with class-validator decorators:
import { Mixin, decorate } from "ts-mixer";
import {
IsString, IsNumber, IsEmail, IsBoolean, IsOptional,
Length, Min, Max, validate
} from "class-validator";
class UserValidation {
@decorate(IsString())
@decorate(Length(3, 20))
username: string = "";
@decorate(IsEmail())
email: string = "";
@decorate(IsNumber())
@decorate(Min(18))
@decorate(Max(120))
age: number = 0;
}
class ProfileValidation {
@decorate(IsString())
@decorate(IsOptional())
@decorate(Length(0, 500))
bio?: string;
@decorate(IsBoolean())
isPublic: boolean = false;
@decorate(IsString())
@decorate(IsOptional())
website?: string;
}
class ValidatedUser extends Mixin(UserValidation, ProfileValidation) {
constructor(data: Partial<ValidatedUser>) {
super();
Object.assign(this, data);
}
async isValid(): Promise<boolean> {
const errors = await validate(this);
return errors.length === 0;
}
}
// Usage with validation
const user = new ValidatedUser({
username: "johndoe",
email: "john@example.com",
age: 25,
bio: "Software developer",
isPublic: true
});
user.isValid().then(valid => {
console.log("User is valid:", valid); // true
});
const invalidUser = new ValidatedUser({
username: "jo", // too short
email: "invalid-email",
age: 15 // too young
});
invalidUser.isValid().then(valid => {
console.log("User is valid:", valid); // false
});Combining different types of decorators in mixed classes:
import { Mixin, decorate } from "ts-mixer";
// Class decorator
function Serializable<T extends { new(...args: any[]): {} }>(constructor: T) {
return class extends constructor {
serialize() {
return JSON.stringify(this);
}
};
}
// Property decorator
function Computed(target: any, propertyKey: string) {
console.log(`Property ${propertyKey} is computed`);
}
// Method decorator
function Cached(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const cache = new Map();
const originalMethod = descriptor.value;
descriptor.value = function(...args: any[]) {
const key = JSON.stringify(args);
if (cache.has(key)) {
return cache.get(key);
}
const result = originalMethod.apply(this, args);
cache.set(key, result);
return result;
};
return descriptor;
}
@decorate(Serializable)
class DataModel {
@decorate(Computed)
id: number = 0;
@decorate(Computed)
createdAt: Date = new Date();
@decorate(Cached)
expensiveCalculation(input: number): number {
console.log("Performing expensive calculation...");
return input * input * input;
}
}
@decorate(Serializable)
class ExtendedModel {
@decorate(Computed)
updatedAt: Date = new Date();
@decorate(Cached)
anotherExpensiveCalc(a: number, b: number): number {
console.log("Another expensive calculation...");
return Math.pow(a, b);
}
}
class CompleteModel extends Mixin(DataModel, ExtendedModel) {
name: string = "Complete Model";
summary(): string {
return `${this.name} created at ${this.createdAt}`;
}
}
const model = new CompleteModel();
console.log(model.serialize()); // Serializable decorator works
console.log(model.expensiveCalculation(5)); // Cached decorator works
console.log(model.expensiveCalculation(5)); // Returns cached resultThe decorator inheritance behavior can be configured via settings.decoratorInheritance:
import { settings } from "ts-mixer";
// Configure decorator inheritance strategy
settings.decoratorInheritance = "deep"; // default - inherit from all ancestors
settings.decoratorInheritance = "direct"; // only from direct mixin classes
settings.decoratorInheritance = "none"; // disable decorator inheritanceStrategies:
"deep" (default): Inherits decorators from all classes in the mixin hierarchy"direct": Only inherits decorators from classes directly passed to Mixin()"none": Disables decorator inheritance completelyimport { Mixin, decorate, settings } from "ts-mixer";
@decorate(SomeDecorator())
class A {}
@decorate(AnotherDecorator())
class B extends A {}
@decorate(ThirdDecorator())
class C {}
settings.decoratorInheritance = "deep";
class DeepMixed extends Mixin(B, C) {}
// Inherits: SomeDecorator (from A), AnotherDecorator (from B), ThirdDecorator (from C)
settings.decoratorInheritance = "direct";
class DirectMixed extends Mixin(B, C) {}
// Inherits: AnotherDecorator (from B), ThirdDecorator (from C)
// Does NOT inherit SomeDecorator from A
settings.decoratorInheritance = "none";
class NoDecorators extends Mixin(B, C) {}
// Inherits: No decoratorsThe decorator system requires ES6 Map support. For older environments, you need a polyfill.
When multiple classes provide decorators for the same target, they are applied in the order the classes are provided to Mixin().
Property and method decorators are correctly categorized as static or instance-level and applied to the appropriate context in the mixed class.
Decorator inheritance adds some overhead during class creation. For performance-critical applications, consider using "direct" or "none" inheritance strategies.
Install with Tessl CLI
npx tessl i tessl/npm-ts-mixer