Mixins in typescript

Mixins in typescript

A mixin is an abstract subclass; i.e. a subclass definition that may be applied to different superclasses to create a related family of modified classes.

Gilad Bracha and William Cook, Mixin-based Inheritance

  • mixin definition: The definition of a class that may be applied to different superclasses.

  • mixin application: The application of a mixin definition to a specific superclass, producing a new subclass.

Mixin libraries like Cocktail, traits.js, and patterns described in many blog posts (like one of the latest to hit Hacker News: Using ES7 Decorators as Mixins), generally work by modifying objects in place, copying in properties from mixin objects and overwriting existing properties.

This is often implemented via a function similar to this:

function mixin(source, target) {
  for (var prop in source) {
    if (source.hasOwnProperty(prop)) {
      target[prop] = source[prop];
    }
  }
}

A version of this has even made it into JavaScript as Object.assign.

mixin() is usually then called on a prototype:

mixin(MyMixin, MyClass.prototype);

and now MyClass has all the properties defined in MyMixin.

functors: function which creates a function

function myloggerFunction() {
  return (str) => {console.log(str)}
}

const logger1 = myloggerFunction();

logger1('hello');

functions that creates class

function myloggerFunction() {
    return class MyLoggerClass() {
        private 
    }
}
function myLogFunction() {
  return (str: string) => {
    console.log(str);
  };
}

function myLoggerClass() {
  return new (class Logger {
    private completeLog: string = "";
    log(str: string) {
      console.log(str);
      this.completeLog += `${str}\n`;
    }
    dumpLog() {
      return this.completeLog;
    }
  })();
}

function SimpleMemoryDatabase<T>() {
  return class SimpleMemoryDatabase {
    private db: Record<string, T> = {};

    set(id: string, value: T): void {
      this.db[id] = value;
    }

    get(id: string): T {
      return this.db[id];
    }

    getObject(): Record<string, T> {
      return this.db;
    }
  };
}

const StringDatabase = SimpleMemoryDatabase<string>();

const sdb1 = new StringDatabase();
sdb1.set("name", "Jack");
console.log(sdb1.get("name"));

type Constructor<T> = new (...args: any[]) => T;

function Dumpable<
  T extends Constructor<{
    getObject(): object;
  }>
>(Base: T) {
  return class Dumpable extends Base {
    dump() {
      console.log(this.getObject());
    }
  };
}

const DumpableStringDatabase = Dumpable(StringDatabase);
const sdb2 = new DumpableStringDatabase();
sdb2.set("name", "Jack");
sdb2.dump();

[A mixin is] a function that

  1. takes a constructor,

  2. creates a class that extends that constructor with new functionality

  3. returns the new class

// Needed for all mixins
type Constructor<T = {}> = new (...args: any[]) => T;

////////////////////
// Example mixins
////////////////////

// A mixin that adds a property
function Timestamped<TBase extends Constructor>(Base: TBase) {
  return class extends Base {
    timestamp = Date.now();
  };
}

// a mixin that adds a property and methods
function Activatable<TBase extends Constructor>(Base: TBase) {
  return class extends Base {
    isActivated = false;

    activate() {
      this.isActivated = true;
    }

    deactivate() {
      this.isActivated = false;
    }
  };
}

////////////////////
// Usage to compose classes
////////////////////

// Simple class
class User {
  name = '';
}

// User that is Timestamped
const TimestampedUser = Timestamped(User);

// User that is Timestamped and Activatable
const TimestampedActivatableUser = Timestamped(Activatable(User));

////////////////////
// Using the composed classes
////////////////////

const timestampedUserExample = new TimestampedUser();
console.log(timestampedUserExample.timestamp);

const timestampedActivatableUserExample = new TimestampedActivatableUser();
console.log(timestampedActivatableUserExample.timestamp);
console.log(timestampedActivatableUserExample.isActivated);
// Define a simple class with a greet method
class Greeter {
  greet(name: string) {
    console.log(`Hello, ${name}!`);
  }
}

// Define a mixin that adds a log method to a class
type Loggable = { log(message: string): void };

function withLogging<T extends new (...args: any[]) => Loggable>(Base: T) {
  return class extends Base {
    log(message: string) {
      console.log(`[${new Date().toISOString()}] ${message}`);
    }
  };
}

// Create a new class that combines the Greeter and Loggable mixins
const MyGreeter = withLogging(Greeter);

// Use the new class to create an instance and call its methods
const greeter = new MyGreeter();
greeter.greet("Alice"); // Output: "Hello, Alice!"
greeter.log("An event occurred."); // Output: "[2023-04-04T12:00:00.000Z] An event occurred."

This is just the beginning of many topics related to mixins in JavaScript. I'll post more about things like:

  • Enhancing Mixins with Decorator Functions new post that covers:

  • Caching mixin applications so that the same mixin applied to the same superclass reuses a prototype.

  • Getting instanceof to work.

  • How mixin inheritance can address the fear that ES6 classes and classical inheritance are bad for JavaScript.

  • Using subclass-factory-style mixins in ES5.

  • De-duplicating mixins so mixin composition is more usable.

References:

https://bryntum.com/blog/the-mixin-pattern-in-typescript-all-you-need-to-know/

https://medium.com/@saif.adnan/typescript-mixin-ee962be3224d

https://egghead.io/lessons/typescript-use-the-optional-chaining-operator-in-typescript

https://javascript.info/mixins

Did you find this article valuable?

Support Coolhead by becoming a sponsor. Any amount is appreciated!