3
\$\begingroup\$

The TypeScript handbook provides a mixin pattern but it requires a bunch of boilerplate, doesn't work for getters/setters and doesn't support defining default values (or any constructor work) in the mixin (only on the composite object).

I wrote an alternative mixin pattern for a charting library I'm working on. My library expects a lot of input and has a lot of output depending on what pieces end up in the final chart. I have a "chart context" and each chart widget can contribute to the chart context with a mixin.

I can currently see a few cons to my approach.

  • It doesn't support mixins which need multiple inheritance (I suppose I could create such a mixin using applyMixins but that seems a little complex).
  • It doesn't support adding methods and functions to the final composite object (this is really just another case of the above point)
  • If multiple classes extend the same base class then that base class' constructor would be called multiple times.

These issues are not a concern for my application but I am not a Javascript expert and this code, though it works, did some pretty deep prototype manipulation. So my main concerns are:

  1. Is this prototype manipulation invalid and thus will cause issues for me in the future?
  2. In the TypeScript example they state:

derivedCtor.prototype[name] = baseCtor.prototype[name];

but it seems like it would be better to say:

Object.defineProperty(derivedCtor.prototype, name, Object.getOwnPropertyDescriptor(baseCtor, name));

why didn't they use this approach? Is there some issue with it?

The Code:

import { Subject } from 'rxjs/Subject';

class HasYAxis {
    yAxisType: 'linear' | 'log' = 'linear';
}

class BaseChart {
    size = { width: 0, height: 0 };
    get hasSize() { return !!this.size && this.size.width > 0 && this.size.height > 0; }
}

class BarChart extends BaseChart {
    bins: number[];
    yAxisType: 'linear' | 'log' = 'linear';
    binSelected = new Subject<number>();
    binHidden = new Subject<number>();
    get binWidth() { return this.size.width / this.bins.length; }
    getTotalBinCount() { return this.bins.reduce((a, b) => a + b, 0); }
}

interface Constructor<T> { new(): T; }

function getParents(ctor: Constructor<any>) {
    const parents = [];
    let parent = Object.getPrototypeOf(ctor.prototype);
    while (parent !== Object.prototype) {
        parents.push(parent);
        parent = Object.getPrototypeOf(parent);
    }
    return parents;
}

function getAllPrototypes(ctors: Constructor<any>[]) {
    const allCtors = new Set<Constructor<any>>();
    for (let ctor of ctors) {
        allCtors.add(ctor.prototype);
        for (let parent of getParents(ctor)) {
            allCtors.add(parent);
        }
    }
    return allCtors;
}

// I'm not sure the name for this.  I call it Christmas tree overloading.
// I've seen it used in a few places where a method could take in a 
// hetergeneous array and the result type depends on the type of the
// inputs.  I'm aware of the obvious cons (lots of typing and gives back
// any if passed in more than 5 arguments) and not too concerned about it.
function applyMixins<T1, T2>(t1: Constructor<T1>, t2: Constructor<T2>): Constructor<T1 & T2>;
function applyMixins<T1, T2, T3>(t1: Constructor<T1>, t2: Constructor<T2>, t3: Constructor<T3>): Constructor<T1 & T2 & T3>;
function applyMixins<T1, T2, T3, T4>(t1: Constructor<T1>, t2: Constructor<T2>, t3: Constructor<T3>, t4: Constructor<T4>): Constructor<T1 & T2 & T3 & T4>;
function applyMixins<T1, T2, T3, T4, T5>(t1: Constructor<T1>, t2: Constructor<T2>, t3: Constructor<T3>, t4: Constructor<T4>, t5: Constructor<T5>): Constructor<T1 & T2 & T3 & T4 & T5>;
function applyMixins(...ctors: Constructor<any>[]) {
    // The constructor for the composite object will simply call the constructors for each of the mixins.  This adds
    // support for defining default values as well as construction initialization in the mixins.
    const result = class {
        constructor() {
            // Don't include parents here, assume base ctors will call super()
            for (let ctor of ctors) {
                ctor.prototype.constructor.apply(this);
            }
        }
    } as any;
    // allProtos contains the prototypes of each of the ctors as well as prototypes for any base types they may have
    const allProtos = getAllPrototypes(ctors);
    allProtos.forEach(proto => {
        Object.getOwnPropertyNames(proto).filter(name => name !== 'constructor').forEach(name => {
            // Here I am using the property descriptor instead of the value.  If I don't (e.g. if I use
            // proto[name] then getter/setter based properties will not work correctly).
            let propertyDescriptor = Object.getOwnPropertyDescriptor(proto, name);
            Object.defineProperty(result.prototype, name, propertyDescriptor);
        });
    });
    return result;
}

let chartContextClass = applyMixins(HasYAxis, BarChart);
let chartContext = new chartContextClass();

chartContext.bins = [1, 2];
chartContext.size = { width: 10, height: 10 };
console.log(chartContext.hasSize);            // true
console.log(chartContext.binWidth);           // 5
console.log(chartContext.getTotalBinCount()); // 3
console.log(chartContext.yAxisType);          // linear
\$\endgroup\$
  • \$\begingroup\$ I assume "The Code" is your version? You can't put someone else's code up for review. \$\endgroup\$ – Mast Mar 28 '18 at 19:07
  • 1
    \$\begingroup\$ Yes, "The Code" is my version. I liked to the standard pattern in the first sentence. \$\endgroup\$ – Pace Mar 28 '18 at 19:46

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Browse other questions tagged or ask your own question.