Typescript, static method inheritance

vincent_chapron picture vincent_chapron · Apr 14, 2017 · Viewed 13.1k times · Source

I'm working with typescript and I have a problem with the static inheritance between classes

Can anyone explain me the result of the following :

class Foo {
    protected static bar: string[] = [];

    public static addBar(bar: string) {
        this.bar.push(bar);
    }

    public static logBar() {
        console.log(this.bar);
    }
}

class Son extends Foo {
    protected static bar: string[] = [];
}

class Daughter extends Foo {}

Foo.addBar('Hello');
Son.addBar('World');
Daughter.addBar('Both ?');
Foo.logBar();
Son.logBar();
Daughter.logBar();

current result :

[ 'Hello', 'Both ?' ]
[ 'World' ]
[ 'Hello', 'Both ?' ]

but I want :

[ 'Hello' ]
[ 'World' ]
[ 'Both ?' ]

Do I have a solution without redeclare the static bar property ?

Thanks !

Answer

T.J. Crowder picture T.J. Crowder · Apr 14, 2017

The key thing to understand about static and class is that the constructor function of the subclass inherits from the constructor function of the superclass. Literally. class doesn't just set up inheritance between instances created by the constructors, the constructors themselves are also in an inheritance structure.

Foo is the prototype of Son and Daughter. That means that Daughter.bar is Foo.bar, it's an inherited property. But you gave Son its own bar property, with its own array, so looking up bar on Son finds Son's own bar, not the one on Foo. Here's a simpler example of that happening:

class Foo { }
class Son extends Foo { }
class Daughter extends Foo { }

Foo.bar = new Map([["a", "ayy"]]);
console.log(Foo.bar.get("a"));          // "ayy"

// `Son` inherits `bar` from `Foo`:
console.log(Son.bar === Foo.bar);       // true, same Map object
console.log(Son.bar.get("a"));          // "ayy"

// So does `Daughter` -- for now
console.log(Daughter.bar === Foo.bar);  // true, same Map object
console.log(Daughter.bar.get("a"));   // "ayy"

// Retroactively giving `Son` its own static `bar`
Son.bar = new Map();
console.log(Son.bar === Foo.bar);       // false, different Map objects
console.log(Son.bar.get("a"));          // undefined

That's why you see ["Hello", "Both ?"] when you look at Foo.bar and Daughter.bar: It's the same bar, pointing at the same array. But you only see ["World"] on Son.bar, because it's a different bar pointing at a different array.

To separate them, you probably want to give each constructor its own bar, although you could do what Nitzan Tomer suggests with a Map.


A bit more detail on how things are organized. It's a bit like this:

const Foo = {};
Foo.bar = [];
const Son = Object.create(Foo);
Son.bar = []; // Overriding Foo's bar
const Daughter = Object.create(Foo);
Foo.bar.push("Hello");
Son.bar.push("World");
Daughter.bar.push("Both ?");
console.log(Foo.bar);
console.log(Son.bar);
console.log(Daughter.bar);

This is a very surprising thing if you come to it fresh, but your three classes look something like this in memory:

                                             +−−>Function.prototype 
                          +−−−−−−−−−−−−−−−+  |
Foo−−−−−−−−−−−−−−−−−−+−+−>|   (function)  |  |       
                    / /   +−−−−−−−−−−−−−−−+  |       
                    | |   | [[Prototype]] |−−+       +−−−−−−−−−−−+
                    | |   | bar           |−−−−−−−−−>|  (array)  |
                    | |   | addBar, etc.  |          +−−−−−−−−−−−+
                    | |   +−−−−−−−−−−−−−−−+          | length: 2 |
                    | |                              | 0: Hello  |
                    | +−−−−−−−−−−−−−+                | 1: Both ? |
                    |               |                +−−−−−−−−−−−+
                    +−−−−−−−−−−−−+  |
                                 |  |
              +−−−−−−−−−−−−−−−+  |  |
              |   (function)  |  |  |    
              +−−−−−−−−−−−−−−−+  |  |    
Daughter−−−−−>| [[Prototype]] |−−+  |
              +−−−−−−−−−−−−−−−+     |
                                    |
              +−−−−−−−−−−−−−−−+     |
              |   (function)  |     |       
              +−−−−−−−−−−−−−−−+     |       
Son−−−−−−−−−−>| [[Prototype]] |−−−−−+    +−−−−−−−−−−−+
              | bar           |−−−−−−−−−>|  (array)  |
              +−−−−−−−−−−−−−−−+          +−−−−−−−−−−−+
                                         | length: 1 |
                                         | 0: World  |
                                         +−−−−−−−−−−−+