Prerequisites
- Strong understanding of classes and inheritance ๐
- Generic types knowledge ๐
- Interface and type manipulation skills ๐ป
What you'll learn
- Understand mixin patterns and composition ๐ฏ
- Create reusable mixin functions ๐๏ธ
- Implement multiple inheritance patterns ๐ก๏ธ
- Build complex class hierarchies with mixins โจ
๐ฏ Introduction
Welcome to the powerful world of mixins! ๐ In this guide, weโll explore how TypeScript enables composition over inheritance through mixins - a pattern that lets you build classes from reusable components.
Youโll discover how mixins are like LEGO blocks ๐งฉ - you can combine different pieces to build exactly what you need! Whether youโre avoiding the limitations of single inheritance ๐ซ, sharing functionality across unrelated classes ๐, or building plugin architectures ๐, understanding mixins is essential for flexible TypeScript design.
By the end of this tutorial, youโll be confidently composing classes from multiple sources and creating maintainable, reusable code architectures! Letโs start mixing! ๐โโ๏ธ
๐ Understanding Mixins
๐ค What are Mixins?
Mixins are a pattern for building classes from reusable components. Instead of using inheritance to share functionality, mixins allow you to compose classes by combining multiple partial class implementations. Itโs like having multiple inheritance without the complexity!
Think of mixins like:
- ๐งฉ LEGO blocks: Snap together different pieces
- ๐จ Color mixing: Combine colors to create new ones
- ๐น Cocktail mixing: Blend ingredients for the perfect drink
- ๐งฌ DNA combination: Mixing traits from multiple sources
๐ก Why Use Mixins?
Hereโs why developers love mixins:
- Composition over Inheritance ๐๏ธ: More flexible than single inheritance
- Reusability โป๏ธ: Share functionality across unrelated classes
- Modularity ๐ฆ: Keep concerns separated
- Avoiding Diamond Problem ๐: No ambiguity in method resolution
Real-world example: Game development ๐ฎ - A character might need Moveable
, Attackable
, and Healable
behaviors, but not all characters need all behaviors!
๐ง Basic Mixin Patterns
๐ Function-Based Mixins
Letโs start with the fundamental mixin pattern:
// ๐ฏ Basic mixin pattern
type Constructor<T = {}> = new (...args: any[]) => T;
// ๐โโ๏ธ Moveable mixin
function Moveable<TBase extends Constructor>(Base: TBase) {
return class Moveable extends Base {
private _x: number = 0;
private _y: number = 0;
private _speed: number = 5;
get position() {
return { x: this._x, y: this._y };
}
moveTo(x: number, y: number): void {
this._x = x;
this._y = y;
console.log(`Moved to (${x}, ${y})`);
}
moveBy(dx: number, dy: number): void {
this._x += dx * this._speed;
this._y += dy * this._speed;
console.log(`Moved by (${dx}, ${dy}) to (${this._x}, ${this._y})`);
}
setSpeed(speed: number): void {
this._speed = speed;
}
};
}
// โ๏ธ Attackable mixin
function Attackable<TBase extends Constructor>(Base: TBase) {
return class Attackable extends Base {
private _attackPower: number = 10;
private _attackRange: number = 1;
attack(target: any): void {
if (this.isInRange(target)) {
console.log(`Attacking with power ${this._attackPower}`);
if ('takeDamage' in target) {
target.takeDamage(this._attackPower);
}
} else {
console.log('Target out of range!');
}
}
setAttackPower(power: number): void {
this._attackPower = power;
}
setAttackRange(range: number): void {
this._attackRange = range;
}
private isInRange(target: any): boolean {
if ('position' in this && 'position' in target) {
const myPos = (this as any).position;
const targetPos = target.position;
const distance = Math.sqrt(
Math.pow(targetPos.x - myPos.x, 2) +
Math.pow(targetPos.y - myPos.y, 2)
);
return distance <= this._attackRange;
}
return true;
}
};
}
// ๐ก๏ธ Defendable mixin
function Defendable<TBase extends Constructor>(Base: TBase) {
return class Defendable extends Base {
private _health: number = 100;
private _maxHealth: number = 100;
private _defense: number = 5;
get health() {
return this._health;
}
get isAlive() {
return this._health > 0;
}
takeDamage(damage: number): void {
const actualDamage = Math.max(0, damage - this._defense);
this._health = Math.max(0, this._health - actualDamage);
console.log(`Took ${actualDamage} damage. Health: ${this._health}/${this._maxHealth}`);
if (!this.isAlive) {
this.onDeath();
}
}
heal(amount: number): void {
this._health = Math.min(this._maxHealth, this._health + amount);
console.log(`Healed ${amount}. Health: ${this._health}/${this._maxHealth}`);
}
setDefense(defense: number): void {
this._defense = defense;
}
protected onDeath(): void {
console.log('Character has died!');
}
};
}
// ๐ฎ Base character class
class Character {
constructor(public name: string) {
console.log(`Created character: ${name}`);
}
greet(): void {
console.log(`Hello, I'm ${this.name}`);
}
}
// ๐๏ธ Compose different character types
class Warrior extends Moveable(Attackable(Defendable(Character))) {
constructor(name: string) {
super(name);
this.setAttackPower(15);
this.setDefense(10);
}
specialAbility(): void {
console.log(`${this.name} uses Berserker Rage!`);
this.setAttackPower(30);
setTimeout(() => this.setAttackPower(15), 5000);
}
}
class Scout extends Moveable(Defendable(Character)) {
constructor(name: string) {
super(name);
this.setSpeed(10); // Scouts are fast!
this.setDefense(3); // But less tanky
}
stealth(): void {
console.log(`${this.name} enters stealth mode`);
}
}
// ๐ซ Usage
const warrior = new Warrior('Thorin');
const scout = new Scout('Legolas');
warrior.greet(); // "Hello, I'm Thorin"
warrior.moveTo(10, 20);
warrior.attack(scout); // Will be out of range
warrior.moveBy(1, 0); // Move closer
warrior.attack(scout); // Now in range!
scout.heal(10);
scout.moveBy(5, 5); // Scouts move faster
scout.stealth();
๐จ Constrained Mixins
Creating mixins with type constraints:
// ๐ฏ Mixins with constraints
interface Timestamped {
timestamp: Date;
}
interface Tagged {
tags: Set<string>;
}
// ๐
Timestamped mixin
function TimestampedMixin<TBase extends Constructor>(Base: TBase) {
return class Timestamped extends Base {
timestamp = new Date();
updateTimestamp(): void {
this.timestamp = new Date();
}
getAge(): number {
return Date.now() - this.timestamp.getTime();
}
getFormattedTime(): string {
return this.timestamp.toISOString();
}
};
}
// ๐ท๏ธ Tagged mixin with constraint
function TaggedMixin<TBase extends Constructor<Timestamped>>(Base: TBase) {
return class Tagged extends Base {
tags = new Set<string>();
addTag(tag: string): void {
this.tags.add(tag);
this.updateTimestamp(); // Can access Timestamped methods!
console.log(`Added tag "${tag}" at ${this.getFormattedTime()}`);
}
removeTag(tag: string): boolean {
const removed = this.tags.delete(tag);
if (removed) {
this.updateTimestamp();
}
return removed;
}
hasTag(tag: string): boolean {
return this.tags.has(tag);
}
getTags(): string[] {
return Array.from(this.tags);
}
};
}
// ๐ Searchable mixin requiring Tagged
function SearchableMixin<TBase extends Constructor<Tagged & Timestamped>>(Base: TBase) {
return class Searchable extends Base {
matches(query: string): boolean {
// Search in tags
for (const tag of this.tags) {
if (tag.toLowerCase().includes(query.toLowerCase())) {
return true;
}
}
return false;
}
matchesAll(queries: string[]): boolean {
return queries.every(query => this.matches(query));
}
matchesAny(queries: string[]): boolean {
return queries.some(query => this.matches(query));
}
getRelevanceScore(query: string): number {
let score = 0;
const lowerQuery = query.toLowerCase();
for (const tag of this.tags) {
const lowerTag = tag.toLowerCase();
if (lowerTag === lowerQuery) score += 10;
else if (lowerTag.includes(lowerQuery)) score += 5;
}
// Boost recent items
const ageInHours = this.getAge() / (1000 * 60 * 60);
if (ageInHours < 1) score += 5;
else if (ageInHours < 24) score += 3;
else if (ageInHours < 168) score += 1;
return score;
}
};
}
// ๐ Document class using constrained mixins
class Document {
constructor(public title: string, public content: string) {}
}
class SmartDocument extends SearchableMixin(TaggedMixin(TimestampedMixin(Document))) {
private _version: number = 1;
updateContent(content: string): void {
this.content = content;
this._version++;
this.updateTimestamp();
this.addTag(`v${this._version}`);
}
getInfo(): string {
return `${this.title} (v${this._version}) - ${this.getTags().length} tags - ${this.getFormattedTime()}`;
}
}
// ๐ซ Usage
const doc = new SmartDocument('TypeScript Guide', 'Learn TypeScript...');
doc.addTag('programming');
doc.addTag('typescript');
doc.addTag('tutorial');
console.log(doc.matches('type')); // true
console.log(doc.getRelevanceScore('typescript')); // High score
console.log(doc.getInfo());
doc.updateContent('Updated content...');
console.log(doc.getTags()); // Includes version tags
๐ Advanced Mixin Patterns
๐๏ธ Parameterized Mixins
Creating configurable mixins:
// ๐ฏ Parameterized mixin factory
interface CacheConfig {
maxSize?: number;
ttl?: number; // Time to live in milliseconds
strategy?: 'lru' | 'fifo';
}
function Cacheable<TBase extends Constructor>(
Base: TBase,
config: CacheConfig = {}
) {
const { maxSize = 100, ttl = 60000, strategy = 'lru' } = config;
return class Cacheable extends Base {
private cache = new Map<string, { value: any; timestamp: number; hits: number }>();
private accessOrder: string[] = [];
protected cacheGet<T>(key: string): T | undefined {
const entry = this.cache.get(key);
if (!entry) return undefined;
// Check TTL
if (Date.now() - entry.timestamp > ttl) {
this.cache.delete(key);
return undefined;
}
// Update access tracking
entry.hits++;
if (strategy === 'lru') {
const index = this.accessOrder.indexOf(key);
if (index > -1) {
this.accessOrder.splice(index, 1);
}
this.accessOrder.push(key);
}
return entry.value as T;
}
protected cacheSet(key: string, value: any): void {
// Evict if necessary
if (this.cache.size >= maxSize) {
this.evict();
}
this.cache.set(key, {
value,
timestamp: Date.now(),
hits: 0
});
if (strategy === 'fifo' || strategy === 'lru') {
this.accessOrder.push(key);
}
}
protected cacheClear(): void {
this.cache.clear();
this.accessOrder = [];
}
protected getCacheStats() {
const entries = Array.from(this.cache.entries());
const totalHits = entries.reduce((sum, [_, entry]) => sum + entry.hits, 0);
return {
size: this.cache.size,
maxSize,
strategy,
ttl,
totalHits,
hitRate: totalHits / Math.max(1, entries.length),
oldestEntry: entries.length > 0
? new Date(Math.min(...entries.map(([_, e]) => e.timestamp)))
: null
};
}
private evict(): void {
if (strategy === 'lru' || strategy === 'fifo') {
const keyToRemove = this.accessOrder.shift();
if (keyToRemove) {
this.cache.delete(keyToRemove);
}
}
}
};
}
// ๐ API client with caching
class APIClient {
constructor(private baseURL: string) {}
protected async fetch(endpoint: string): Promise<any> {
console.log(`Fetching: ${this.baseURL}${endpoint}`);
// Simulate API call
return { data: `Response from ${endpoint}`, timestamp: Date.now() };
}
}
// Small cache with short TTL
class CachedAPIClient extends Cacheable(APIClient, { maxSize: 50, ttl: 5000 }) {
async get(endpoint: string): Promise<any> {
const cached = this.cacheGet<any>(endpoint);
if (cached) {
console.log(`Cache hit: ${endpoint}`);
return cached;
}
const data = await this.fetch(endpoint);
this.cacheSet(endpoint, data);
return data;
}
getStats() {
return this.getCacheStats();
}
}
// Large cache with long TTL
class DataStore extends Cacheable(APIClient, { maxSize: 1000, ttl: 3600000, strategy: 'lru' }) {
async loadData(key: string): Promise<any> {
const cached = this.cacheGet<any>(key);
if (cached) return cached;
const data = await this.fetch(`/data/${key}`);
this.cacheSet(key, data);
return data;
}
}
๐ญ Multiple Mixin Composition
Advanced patterns for composing multiple mixins:
// ๐ฏ Event emitter mixin
type EventMap = Record<string, any[]>;
function EventEmitterMixin<TBase extends Constructor>(Base: TBase) {
return class EventEmitter extends Base {
private listeners = new Map<string, Set<Function>>();
on(event: string, handler: Function): void {
if (!this.listeners.has(event)) {
this.listeners.set(event, new Set());
}
this.listeners.get(event)!.add(handler);
}
off(event: string, handler: Function): void {
this.listeners.get(event)?.delete(handler);
}
emit(event: string, ...args: any[]): void {
this.listeners.get(event)?.forEach(handler => {
handler.apply(this, args);
});
}
once(event: string, handler: Function): void {
const wrappedHandler = (...args: any[]) => {
handler.apply(this, args);
this.off(event, wrappedHandler);
};
this.on(event, wrappedHandler);
}
};
}
// ๐ State machine mixin
interface StateConfig<TStates extends string> {
initial: TStates;
transitions: Record<TStates, TStates[]>;
}
function StateMachineMixin<
TBase extends Constructor,
TStates extends string
>(Base: TBase, config: StateConfig<TStates>) {
return class StateMachine extends Base {
private currentState: TStates = config.initial;
private stateHistory: TStates[] = [config.initial];
getState(): TStates {
return this.currentState;
}
canTransition(to: TStates): boolean {
const allowedTransitions = config.transitions[this.currentState];
return allowedTransitions?.includes(to) ?? false;
}
transition(to: TStates): boolean {
if (!this.canTransition(to)) {
console.warn(`Invalid transition from ${this.currentState} to ${to}`);
return false;
}
const from = this.currentState;
this.currentState = to;
this.stateHistory.push(to);
// Emit event if available
if ('emit' in this) {
(this as any).emit('stateChange', { from, to });
}
return true;
}
getStateHistory(): TStates[] {
return [...this.stateHistory];
}
resetState(): void {
this.currentState = config.initial;
this.stateHistory = [config.initial];
}
};
}
// ๐ฆ Observable mixin
function ObservableMixin<TBase extends Constructor>(Base: TBase) {
return class Observable extends Base {
private observers = new Map<string, Set<(value: any) => void>>();
private values = new Map<string, any>();
observe<T>(property: string, callback: (value: T) => void): () => void {
if (!this.observers.has(property)) {
this.observers.set(property, new Set());
}
this.observers.get(property)!.add(callback);
// Call with current value if exists
if (this.values.has(property)) {
callback(this.values.get(property));
}
// Return unsubscribe function
return () => {
this.observers.get(property)?.delete(callback);
};
}
protected notify(property: string, value: any): void {
this.values.set(property, value);
this.observers.get(property)?.forEach(callback => {
callback(value);
});
}
};
}
// ๐ฎ Game character with multiple mixins
type CharacterState = 'idle' | 'moving' | 'attacking' | 'defending' | 'dead';
class BaseCharacter {
constructor(public name: string, public type: string) {}
}
// Compose all mixins
class GameCharacter extends ObservableMixin(
EventEmitterMixin(
StateMachineMixin(
Moveable(
Attackable(
Defendable(BaseCharacter)
)
),
{
initial: 'idle' as CharacterState,
transitions: {
idle: ['moving', 'attacking', 'defending'],
moving: ['idle', 'attacking', 'defending'],
attacking: ['idle', 'moving'],
defending: ['idle', 'moving'],
dead: []
}
}
)
)
) {
constructor(name: string, type: string) {
super(name, type);
// Set up state change notifications
this.on('stateChange', ({ from, to }) => {
console.log(`${this.name} transitioned from ${from} to ${to}`);
this.notify('state', to);
});
// Override death handler
this.on('death', () => {
this.transition('dead' as CharacterState);
});
}
// Override methods to integrate state
moveTo(x: number, y: number): void {
if (this.getState() === 'dead') {
console.log(`${this.name} cannot move while dead!`);
return;
}
this.transition('moving' as CharacterState);
super.moveTo(x, y);
setTimeout(() => {
if (this.getState() === 'moving') {
this.transition('idle' as CharacterState);
}
}, 1000);
}
attack(target: any): void {
if (this.getState() === 'dead') {
console.log(`${this.name} cannot attack while dead!`);
return;
}
if (this.transition('attacking' as CharacterState)) {
super.attack(target);
setTimeout(() => {
if (this.getState() === 'attacking') {
this.transition('idle' as CharacterState);
}
}, 500);
}
}
defend(): void {
if (this.getState() === 'dead') return;
if (this.transition('defending' as CharacterState)) {
this.setDefense(this.getDefense() * 2);
setTimeout(() => {
if (this.getState() === 'defending') {
this.setDefense(this.getDefense() / 2);
this.transition('idle' as CharacterState);
}
}, 3000);
}
}
protected onDeath(): void {
super.onDeath();
this.emit('death');
}
private getDefense(): number {
// Access private property through any (mixin limitation)
return (this as any)._defense ?? 5;
}
}
// ๐ซ Usage with all features
const hero = new GameCharacter('Aragorn', 'Warrior');
// Observe state changes
hero.observe('state', (state: CharacterState) => {
console.log(`UI Update: ${hero.name} is now ${state}`);
});
// Listen to events
hero.on('stateChange', ({ to }) => {
if (to === 'attacking') {
console.log('โ๏ธ Battle music starts playing!');
}
});
// Use the character
hero.moveTo(10, 10);
hero.attack(null); // Will fail - not idle
setTimeout(() => {
hero.attack(null); // Will work - back to idle
hero.defend();
}, 1500);
๐ช Property Mixins
๐ง Mixing in Properties
Adding properties and getters/setters via mixins:
// ๐ฏ Property decorator mixin
interface PropertyDescriptor<T> {
default?: T;
validator?: (value: T) => boolean;
transformer?: (value: T) => T;
}
function WithProperty<
TBase extends Constructor,
TProps extends Record<string, PropertyDescriptor<any>>
>(Base: TBase, properties: TProps) {
const NewClass = class extends Base {
private _propertyValues = new Map<string, any>();
private _propertyMetadata = properties;
constructor(...args: any[]) {
super(...args);
// Initialize default values
Object.entries(properties).forEach(([key, descriptor]) => {
if (descriptor.default !== undefined) {
this._propertyValues.set(key, descriptor.default);
}
});
// Create getters and setters
Object.keys(properties).forEach(key => {
Object.defineProperty(this, key, {
get() {
return this._propertyValues.get(key);
},
set(value: any) {
const descriptor = this._propertyMetadata[key];
// Validate
if (descriptor.validator && !descriptor.validator(value)) {
throw new Error(`Invalid value for ${key}: ${value}`);
}
// Transform
const finalValue = descriptor.transformer
? descriptor.transformer(value)
: value;
const oldValue = this._propertyValues.get(key);
this._propertyValues.set(key, finalValue);
// Notify if available
if ('emit' in this) {
(this as any).emit('propertyChange', {
property: key,
oldValue,
newValue: finalValue
});
}
},
enumerable: true,
configurable: true
});
});
}
getProperties(): Record<string, any> {
const result: Record<string, any> = {};
this._propertyValues.forEach((value, key) => {
result[key] = value;
});
return result;
}
setProperties(values: Partial<Record<keyof TProps, any>>): void {
Object.entries(values).forEach(([key, value]) => {
if (key in this) {
(this as any)[key] = value;
}
});
}
};
return NewClass as typeof NewClass & {
new (...args: any[]): InstanceType<TBase> & {
[K in keyof TProps]: TProps[K]['default'] extends infer D
? D extends undefined ? any : D
: any;
};
};
}
// ๐ Example: Product class with validated properties
class Product {
constructor(public id: string) {}
}
const EnhancedProduct = WithProperty(
EventEmitterMixin(Product),
{
name: {
default: '',
validator: (v: string) => v.length > 0 && v.length <= 100,
transformer: (v: string) => v.trim()
},
price: {
default: 0,
validator: (v: number) => v >= 0,
transformer: (v: number) => Math.round(v * 100) / 100
},
stock: {
default: 0,
validator: (v: number) => v >= 0 && Number.isInteger(v)
},
category: {
default: 'uncategorized',
validator: (v: string) => ['electronics', 'clothing', 'food', 'uncategorized'].includes(v)
},
active: {
default: true,
validator: (v: boolean) => typeof v === 'boolean'
}
}
);
// ๐ซ Usage
const product = new EnhancedProduct('prod-001');
// Listen to property changes
product.on('propertyChange', ({ property, oldValue, newValue }) => {
console.log(`${property} changed from ${oldValue} to ${newValue}`);
});
// Use properties with validation
product.name = ' Laptop '; // Will be trimmed
product.price = 999.999; // Will be rounded to 999.99
product.stock = 10;
product.category = 'electronics';
console.log(product.getProperties());
// This will throw an error
try {
product.price = -10; // Invalid!
} catch (e) {
console.error('Validation error:', e.message);
}
๐ก๏ธ Type-Safe Mixin Patterns
๐ Ensuring Type Safety
Advanced type patterns for mixins:
// ๐ฏ Type-safe mixin with method requirements
interface Identifiable {
id: string;
}
interface Nameable {
name: string;
}
// Mixin that requires certain methods
function AuditableMixin<
TBase extends Constructor<Identifiable & Nameable>
>(Base: TBase) {
return class Auditable extends Base {
private auditLog: Array<{
action: string;
timestamp: Date;
details?: any;
}> = [];
protected audit(action: string, details?: any): void {
this.auditLog.push({
action,
timestamp: new Date(),
details: {
...details,
entityId: this.id,
entityName: this.name
}
});
}
getAuditLog() {
return [...this.auditLog];
}
getLastAudit() {
return this.auditLog[this.auditLog.length - 1];
}
clearAuditLog(): void {
const count = this.auditLog.length;
this.auditLog = [];
this.audit('audit_cleared', { entriesRemoved: count });
}
};
}
// ๐ Permission mixin with type constraints
type Permission = string;
type Role = {
name: string;
permissions: Set<Permission>;
};
interface Authorizable {
roles: Set<Role>;
}
function AuthorizationMixin<
TBase extends Constructor<Identifiable & Auditable>
>(Base: TBase) {
type Auditable = InstanceType<ReturnType<typeof AuditableMixin>>;
return class Authorization extends Base implements Authorizable {
roles = new Set<Role>();
addRole(role: Role): void {
this.roles.add(role);
this.audit('role_added', { role: role.name });
}
removeRole(role: Role): boolean {
const removed = this.roles.delete(role);
if (removed) {
this.audit('role_removed', { role: role.name });
}
return removed;
}
hasPermission(permission: Permission): boolean {
for (const role of this.roles) {
if (role.permissions.has(permission)) {
return true;
}
}
return false;
}
checkPermission(permission: Permission): void {
if (!this.hasPermission(permission)) {
this.audit('permission_denied', { permission });
throw new Error(`Permission denied: ${permission}`);
}
this.audit('permission_granted', { permission });
}
getPermissions(): Set<Permission> {
const allPermissions = new Set<Permission>();
for (const role of this.roles) {
role.permissions.forEach(p => allPermissions.add(p));
}
return allPermissions;
}
};
}
// ๐ข User class with multiple mixins
class User implements Identifiable, Nameable {
constructor(
public id: string,
public name: string,
public email: string
) {}
}
class SecureUser extends AuthorizationMixin(AuditableMixin(User)) {
private data = new Map<string, any>();
setData(key: string, value: any): void {
this.checkPermission('data:write');
this.data.set(key, value);
this.audit('data_updated', { key });
}
getData(key: string): any {
this.checkPermission('data:read');
this.audit('data_accessed', { key });
return this.data.get(key);
}
deleteData(key: string): boolean {
this.checkPermission('data:delete');
const deleted = this.data.delete(key);
this.audit('data_deleted', { key, success: deleted });
return deleted;
}
}
// ๐ซ Usage
const adminRole: Role = {
name: 'admin',
permissions: new Set(['data:read', 'data:write', 'data:delete'])
};
const readOnlyRole: Role = {
name: 'readonly',
permissions: new Set(['data:read'])
};
const user = new SecureUser('user-001', 'Alice', '[email protected]');
user.addRole(readOnlyRole);
// This works
user.getData('someKey');
// This throws an error
try {
user.setData('key', 'value'); // Permission denied!
} catch (e) {
console.error(e.message);
}
// Add admin role
user.addRole(adminRole);
user.setData('key', 'value'); // Now it works!
// Check audit log
console.log(user.getAuditLog());
๐ฎ Hands-On Exercise
Letโs build a game item system using mixins!
๐ Challenge: RPG Item System
Create an item system that:
- Uses mixins for different item properties
- Supports equipment, consumables, and quest items
- Implements rarity and enhancement systems
- Maintains type safety throughout
// Your challenge: Implement this item system
type ItemRarity = 'common' | 'uncommon' | 'rare' | 'epic' | 'legendary';
type ItemType = 'weapon' | 'armor' | 'consumable' | 'quest' | 'material';
interface BaseItem {
id: string;
name: string;
type: ItemType;
}
// Mixins to implement:
// 1. Rareable - adds rarity with color coding
// 2. Stackable - allows items to stack with max stack size
// 3. Enhanceable - allows upgrading with enhancement level
// 4. Useable - adds use functionality with cooldowns
// 5. Tradeable - adds trading properties and restrictions
// 6. Bindable - adds binding to character (soulbound)
// Example usage to support:
const sword = new EnhanceableWeapon('sword-001', 'Excalibur');
sword.enhance(); // +1
sword.setRarity('legendary');
const potion = new StackableConsumable('potion-001', 'Health Potion');
potion.stack(5);
potion.use(); // Reduces stack by 1
const questItem = new BindableQuestItem('quest-001', 'Ancient Scroll');
questItem.bindToCharacter('hero-001');
questItem.isBound(); // true
// Implement the mixin system!
๐ก Solution
Click to see the solution
// Base types
type ItemRarity = 'common' | 'uncommon' | 'rare' | 'epic' | 'legendary';
type ItemType = 'weapon' | 'armor' | 'consumable' | 'quest' | 'material';
interface BaseItem {
id: string;
name: string;
type: ItemType;
}
type Constructor<T = {}> = new (...args: any[]) => T;
// ๐ Rareable mixin
function Rareable<TBase extends Constructor<BaseItem>>(Base: TBase) {
return class Rareable extends Base {
private _rarity: ItemRarity = 'common';
get rarity(): ItemRarity {
return this._rarity;
}
setRarity(rarity: ItemRarity): void {
this._rarity = rarity;
}
getRarityColor(): string {
const colors: Record<ItemRarity, string> = {
common: '#808080',
uncommon: '#00FF00',
rare: '#0080FF',
epic: '#B000B0',
legendary: '#FF8000'
};
return colors[this._rarity];
}
getRarityMultiplier(): number {
const multipliers: Record<ItemRarity, number> = {
common: 1,
uncommon: 1.2,
rare: 1.5,
epic: 2,
legendary: 3
};
return multipliers[this._rarity];
}
getDisplayName(): string {
return `${this.name} [${this._rarity.toUpperCase()}]`;
}
};
}
// ๐ฆ Stackable mixin
function Stackable<TBase extends Constructor<BaseItem>>(
Base: TBase,
maxStack: number = 99
) {
return class Stackable extends Base {
private _quantity: number = 1;
get quantity(): number {
return this._quantity;
}
get maxStackSize(): number {
return maxStack;
}
stack(amount: number): boolean {
const newQuantity = this._quantity + amount;
if (newQuantity > maxStack || newQuantity < 0) {
return false;
}
this._quantity = newQuantity;
return true;
}
split(amount: number): Stackable | null {
if (amount >= this._quantity || amount <= 0) {
return null;
}
this._quantity -= amount;
const splitItem = new (this.constructor as any)(this.id + '_split', this.name);
splitItem._quantity = amount;
return splitItem;
}
canStackWith(other: any): boolean {
return other instanceof Stackable &&
other.id === this.id &&
other.name === this.name &&
this._quantity + other.quantity <= maxStack;
}
mergeStack(other: Stackable): boolean {
if (!this.canStackWith(other)) return false;
this._quantity += other.quantity;
other._quantity = 0;
return true;
}
};
}
// โ๏ธ Enhanceable mixin
function Enhanceable<TBase extends Constructor<BaseItem>>(Base: TBase) {
return class Enhanceable extends Base {
private _enhancementLevel: number = 0;
private _maxEnhancement: number = 10;
private _enhancementHistory: Array<{ level: number; success: boolean; timestamp: Date }> = [];
get enhancementLevel(): number {
return this._enhancementLevel;
}
enhance(): boolean {
if (this._enhancementLevel >= this._maxEnhancement) {
console.log('Max enhancement reached!');
return false;
}
// Success rate decreases with level
const successRate = Math.max(0.1, 1 - (this._enhancementLevel * 0.1));
const success = Math.random() < successRate;
this._enhancementHistory.push({
level: this._enhancementLevel,
success,
timestamp: new Date()
});
if (success) {
this._enhancementLevel++;
console.log(`Enhancement successful! Now +${this._enhancementLevel}`);
} else {
console.log('Enhancement failed!');
// Could break item at high levels
if (this._enhancementLevel > 7 && Math.random() < 0.1) {
this._enhancementLevel = Math.max(0, this._enhancementLevel - 1);
console.log('Item degraded!');
}
}
return success;
}
getEnhancedName(): string {
return this._enhancementLevel > 0
? `${this.name} +${this._enhancementLevel}`
: this.name;
}
getEnhancementBonus(): number {
return this._enhancementLevel * 0.1; // 10% per level
}
getEnhancementHistory() {
return [...this._enhancementHistory];
}
};
}
// ๐พ Useable mixin
function Useable<TBase extends Constructor<BaseItem>>(
Base: TBase,
cooldown: number = 0
) {
return class Useable extends Base {
private _lastUsed: Date | null = null;
private _uses: number = 0;
private _cooldownMs: number = cooldown * 1000;
use(): boolean {
if (!this.canUse()) {
const remaining = this.getCooldownRemaining();
console.log(`On cooldown! ${remaining}ms remaining`);
return false;
}
this._lastUsed = new Date();
this._uses++;
this.onUse();
// Reduce stack if stackable
if ('quantity' in this && 'stack' in this) {
(this as any).stack(-1);
}
return true;
}
canUse(): boolean {
if (!this._lastUsed) return true;
const elapsed = Date.now() - this._lastUsed.getTime();
return elapsed >= this._cooldownMs;
}
getCooldownRemaining(): number {
if (!this._lastUsed) return 0;
const elapsed = Date.now() - this._lastUsed.getTime();
return Math.max(0, this._cooldownMs - elapsed);
}
getUses(): number {
return this._uses;
}
protected onUse(): void {
console.log(`Used ${this.name}`);
}
};
}
// ๐ฐ Tradeable mixin
function Tradeable<TBase extends Constructor<BaseItem>>(Base: TBase) {
return class Tradeable extends Base {
private _tradeable: boolean = true;
private _minLevel: number = 0;
private _value: number = 0;
get tradeable(): boolean {
return this._tradeable;
}
setTradeable(tradeable: boolean): void {
this._tradeable = tradeable;
}
get value(): number {
let baseValue = this._value;
// Adjust for rarity
if ('getRarityMultiplier' in this) {
baseValue *= (this as any).getRarityMultiplier();
}
// Adjust for enhancement
if ('getEnhancementBonus' in this) {
baseValue *= (1 + (this as any).getEnhancementBonus());
}
return Math.floor(baseValue);
}
setValue(value: number): void {
this._value = value;
}
setMinLevel(level: number): void {
this._minLevel = level;
}
canTrade(characterLevel: number): boolean {
return this._tradeable && characterLevel >= this._minLevel;
}
};
}
// ๐ Bindable mixin
function Bindable<TBase extends Constructor<BaseItem>>(Base: TBase) {
return class Bindable extends Base {
private _boundTo: string | null = null;
private _bindOnPickup: boolean = false;
private _bindOnEquip: boolean = false;
bindToCharacter(characterId: string): boolean {
if (this._boundTo && this._boundTo !== characterId) {
return false;
}
this._boundTo = characterId;
// Make untradeable when bound
if ('setTradeable' in this) {
(this as any).setTradeable(false);
}
return true;
}
unbind(): boolean {
if (!this._boundTo) return false;
this._boundTo = null;
return true;
}
isBound(): boolean {
return this._boundTo !== null;
}
getBoundCharacter(): string | null {
return this._boundTo;
}
setBindOnPickup(bind: boolean): void {
this._bindOnPickup = bind;
}
setBindOnEquip(bind: boolean): void {
this._bindOnEquip = bind;
}
};
}
// ๐ก๏ธ Item classes
class Item implements BaseItem {
constructor(
public id: string,
public name: string,
public type: ItemType
) {}
}
// Weapon class
class EnhanceableWeapon extends Tradeable(Enhanceable(Rareable(Item))) {
private _damage: number = 10;
constructor(id: string, name: string) {
super(id, name, 'weapon');
this.setValue(100);
}
getDamage(): number {
const baseDamage = this._damage;
const rarityBonus = this.getRarityMultiplier();
const enhancementBonus = 1 + this.getEnhancementBonus();
return Math.floor(baseDamage * rarityBonus * enhancementBonus);
}
}
// Consumable class
class StackableConsumable extends Useable(Stackable(Tradeable(Item), 20), 5) {
constructor(id: string, name: string) {
super(id, name, 'consumable');
this.setValue(10);
}
protected onUse(): void {
console.log(`Consumed ${this.name}. ${(this as any).quantity - 1} remaining`);
}
}
// Quest item class
class BindableQuestItem extends Bindable(Rareable(Item)) {
private _questId: string;
constructor(id: string, name: string, questId: string) {
super(id, name, 'quest');
this._questId = questId;
this.setBindOnPickup(true);
}
getQuestId(): string {
return this._questId;
}
}
// ๐ซ Usage example
console.log('=== RPG Item System Demo ===\n');
// Create legendary sword
const sword = new EnhanceableWeapon('sword-001', 'Excalibur');
sword.setRarity('legendary');
console.log(`Created: ${sword.getDisplayName()}`);
console.log(`Base damage: ${sword.getDamage()}`);
// Enhance it
for (let i = 0; i < 5; i++) {
sword.enhance();
}
console.log(`Enhanced damage: ${sword.getDamage()}`);
console.log(`Value: ${sword.value} gold\n`);
// Create health potions
const potion = new StackableConsumable('potion-001', 'Health Potion');
potion.stack(4); // Now have 5 potions
console.log(`Created: ${potion.name} x${potion.quantity}`);
// Use potions
potion.use();
console.log(`After use: ${potion.quantity} remaining`);
console.log(`Can use again: ${potion.canUse()} (cooldown: ${potion.getCooldownRemaining()}ms)\n`);
// Create quest item
const questItem = new BindableQuestItem('quest-001', 'Ancient Scroll', 'main-quest-01');
questItem.setRarity('epic');
console.log(`Created: ${questItem.getDisplayName()}`);
questItem.bindToCharacter('hero-001');
console.log(`Bound to: ${questItem.getBoundCharacter()}`);
console.log(`Is bound: ${questItem.isBound()}`);
๐ฏ Summary
Youโve mastered mixins in TypeScript! ๐ You learned how to:
- ๐งฉ Create reusable mixin functions
- ๐๏ธ Compose classes from multiple sources
- ๐จ Use parameterized and constrained mixins
- ๐ Maintain type safety with complex compositions
- ๐ญ Implement multiple inheritance patterns
- โจ Build flexible, maintainable architectures
Mixins provide a powerful alternative to traditional inheritance, enabling you to build complex class hierarchies through composition rather than inheritance chains!