javascripttypescriptdynamicsubclassmixins

TypeScript/JavaScript terser mix-ins


There is an article on using and creating mix-ins on MDN (https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes/extends#mix-ins). I've tried to implement this in TypeScript:

type Constructor = new (...args: any) => any;

function nameMixin(Base: Constructor) {
  return class extends Base {
    #name?: string;

    name() {
      return this.#name;
    }

    setName(name: string) {
      this.#name = name;
    }
  };
}

class None {}
class Foo extends nameMixin(None) {
  constructor(private value: number) {
    super();
  }
}

const foo = new Foo(10)
foo.setName("john")
foo.name()

This seems to work pretty well but the one thing I don't particularly like about this trick is that I have to extend the arbitrary None class.

I've seen other people implement mix-ins with Object.assign(Foo.prototype, mixin), where mixin is an object, but I don't like that because it decouples the mix-in from the declaration.

Anyone know any other, cleaner ways of implementing mix-ins?


Solution

  • "Anyone know any other, cleaner ways of implementing mix-ins?"

    The, in my opinion, still most versatile pattern of the many different ways of accomplishing mixins in JavaScript is a function based mixin which is aware of a this context, thus it always gets applied via call to the object in need of a mixed-in behavior.

    In addition of always working upon a delegated/bound this, one, at apply time, can either pass variables like string and number values or even object-references which could be used as privately shared (even mutable) state amongst other such alike implemented mixins and the classes/factories which do create objects with mixed-in behavior.

    // `this` context aware function based mixin.
    function withSetGetName(name = '') {
    
      // delegated/bound `this` context.
      Reflect.defineProperty(this, 'name', {
    
        // returning the locally scoped variable.
        get: () => name,
    
        // - controlling whether and how to change the
        //   locally scoped (private) variable's value.
        set: (value) => (name = value),
      });
      return this;
    }
    
    class Foo {
      #number;
    
      constructor(name, number) {
        this.#number = number;
    
        // applying the function based mixin.
        withSetGetName.call(this, name);
      }
    }
    
    // instance of class which does mixin-in.
    const foo = new Foo('Jim', 10);
    
    // a plain object with mixed-in behavior.
    const bar = withSetGetName.call({});
    
    console.log(
      'foo.name =>', foo.name
    );
    console.log(
      "foo.name = 'John' =>", (foo.name = 'John')
    );
    console.log(
      'foo.name =>', foo.name
    );
    
    console.log(
      'bar.name =>', bar.name
    );
    console.log(
      "bar.name = 'Jane' =>", (bar.name = 'Jane')
    );
    console.log(
      'bar.name =>', bar.name
    );
    .as-console-wrapper { min-height: 100%!important; top: 0; }