-4

Advanced JavaScript Inheritance in TypeScript

One of the wonderful aspects of JavaScript is the number of different ways objects can inherit encapsulation from others. However, TypeScript puts heavy constraints on the possibilities for writing a module, from this perspective.

In JavaScript, you have the option to achieve Multiple Inheritance -- rather, Mixin functionality -- through the use of Constructor Hijacking, which is an extremely powerful feature of the language:

var Base = function Base() {

    function f() {
        console.log('datum: %s', this.datum);
    }

    function method() {
        this.a();
        this.b();
        this.c();
    }

    // export precepts
    this.datum = true;
    this.f = f;
    this.method = method;

    return this;
};

var A = function A() {

    function a() {
        this.f();
    }

    // export precepts
    this.a = a;

    return this;
};
var B = function B() {

    function b() {
        this.f();
    }

    // export precepts
    this.b = b;

    return this;
};
var C = function C() {

    function c() {
        this.f();
    }

    // export precepts
    this.c = c;

    return this;
};

var Klass = function Klass() {
    var config = { };

    function init() {
        this.method();
    }

    // export precepts
    Base.call(this);
    A.call(this);
    B.call(this);
    C.call(this);
    this.config = config;
    this.init = init;

    return this;
};

var klass = new Klass();
klass.init();
// > datum: true
// > datum: true
// > datum: true

This allows a developer to break their code out in discrete modules that only need follow a pattern or convention in order to extend another module, keeping the Single Responsibility Principle and the Open-Close Principle of SOLID pristine.

Analysis

The code above should log the string datum: true 3 times. This is because Klass decorates (or mixes-in) the Base class so that classes A-C do not throw a Run-Time Error as they invoke this.f. The CallStack of the process looks something like the following:

  • [Klass] init
  • [Base] method
  • [A] a
  • [Base] f
  • [Console] log
  • [B] b
  • [Base] f
  • [Console] log
  • [C] c
  • [Base] f
  • [Console] log

Notes

The CallStack is fairly arbitrary and trivial. Also, this code could be seen as non-SOLID, but just assume we're using The Template-Method Pattern if it helps to get around the crudeness of the example.

Also, there will undoubtedly be a soul to pipe up and say something about how everything we know about the example above violates TypeScript. Bear in mind, "TypeScript is a superset of JavaScript" is simply, plainly, and blatantly wrong -- I won't even argue why, you should already know this if you are using TypeScript.

Question:

Given the code above, how can one achieve such functionality using valid TypeScript syntax? I am also open to leveraging Design Patterns if necessary, though this is still not ideal.

Cody
  • 9,785
  • 4
  • 61
  • 46
  • That's not a call *stack*. If you want to show what is invoked in which order by whom, you'd need a call *tree*. – Bergi Oct 01 '18 at 19:05
  • The code you have might violate TypeScript design guidelines, but it hardly violates TypeScript syntax - every JS program is a valid TypeScript program. – Bergi Oct 01 '18 at 19:06
  • 1
    @Bergi I agree with the first half of your comment, but the word "valid" could be misleading. The principle (I don't know if there are minor exceptions) is that every JS program is a _syntactically_ valid TypeScript program that compiles to a JS program equivalent to the original, but the TypeScript program may have type errors. – Matt McCutchen Oct 01 '18 at 21:06
  • @MattMcCutchen IIIRC, without any type annotations everything is of type `Any` and there won't be errors? – Bergi Oct 01 '18 at 21:08
  • @Bergi No, literals (primitive, array, object, and function) still have some type information, so for example, `(2).slice()` is a type error. Yours seems to be a common misconception; do you have an idea where we should update the documentation to refute it? – Matt McCutchen Oct 01 '18 at 21:14
  • @MattMcCutchen Dunno, I don't think it's anywhere in the documentation. Maybe I'm even confusing it with the Flow type checker. But yeah, it [seems](https://stackoverflow.com/a/44967932/1048572) [to be](https://stackoverflow.com/q/31634118/1048572) [common](https://stackoverflow.com/q/29918324/1048572). Maybe it came from the initial advice on how to adopt typescript - "*just take the current JS code and start to add type annotations bit by bit*". (Of course that would imply a *working* JS code, not one that threw a runtime type error) – Bergi Oct 01 '18 at 21:27

1 Answers1

0

Mixins give you most of what you want. Your example would be:

class Base { 
    datum = true;
    // We can't make these methods abstract because TypeScript currently
    // doesn't support tracking whether mixins implement abstract methods. 
    a() {
        throw new Error("not implemented");
    }
    b() {
        throw new Error("not implemented");
    }
    c() {
        throw new Error("not implemented");
    }
    f() {
        console.log('datum: %s', this.datum);
    }
    method() { 
        this.a();
        this.b();
        this.c();
    }
}

function mixA<Orig extends {new(...args: any[]): Base}>(base: Orig) { 
    return class extends base {
        a() {
            this.f();
        }
    };
}
function mixB<Orig extends {new(...args: any[]): Base}>(base: Orig) { 
    return class extends base {
        b() {
            this.f();
        }
    };
}
function mixC<Orig extends {new(...args: any[]): Base}>(base: Orig) { 
    return class extends base {
        c() {
            this.f();
        }
    };
}

class Klass extends mixA(mixB(mixC(Base))) { 
    config = {};
    init() { 
        this.method();
    }
}

var klass = new Klass();
klass.init();
Matt McCutchen
  • 28,856
  • 2
  • 68
  • 75