20

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 !

2
  • remove the static Commented Apr 14, 2017 at 18:29
  • 3
    Removing the static leaves the property bound to an instance rather than the type. OP wants the property bound to the type. Commented Jan 11, 2020 at 16:40

2 Answers 2

32

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 ? |
                               | |                          +−−−−−−−−−−−+
           +−−−−−−−−−−−−−−−+   | |
Daughter−−>|   (function)  |   | |
           +−−−−−−−−−−−−−−−+   | |
           | [[Prototype]] |−−−+ |
           +−−−−−−−−−−−−−−−+     |
                                 |
           +−−−−−−−−−−−−−−−+     |
Son−−−−−−−>|   (function)  |     |
           +−−−−−−−−−−−−−−−+     |
           | [[Prototype]] |−−−−−+  +−−−−−−−−−−−+
           | bar           |−−−−−−−>|  (array)  |
           +−−−−−−−−−−−−−−−+        +−−−−−−−−−−−+
                                    | length: 1 |
                                    | 0: World  |
                                    +−−−−−−−−−−−+
Sign up to request clarification or add additional context in comments.

3 Comments

You say "that link with Foo is broken" but I think the technical term is "shadowed", right? That is, Foo.bar and Son.bar are two separate objects and Foo.bar is still in Son.bar's prototype chain, but the child object shadows the parent's object, in the same way that an inner scope can shadow a variable in an outer scope. (I say "I think" because I'm not an expert, but it's how I understand things and it helped me a lot.)
@Coderer - I've don't think I've heard the term shadowed used with object properties (just variables in nested scope), but it is conceptually similar, yes. Because Son has its own bar, looking up bar on Son stops when it finds it on Son and doesn't continue along the prototype chain; that's very similar to nested identifier resolution. So I haven't heard it used that way, but...I think it works, sure. :-) Re "...Foo.bar and Son.bar are two separate objects and Foo.bar is still in Son.bar's prototype chain..." I don't think you meant to have those .bars on there, right?
Yes, too late for me to edit, but I meant that to read "Foo.bar is in Son's prototype chain", as in Son has access to Son.bar and Foo.bar, if you're in a context where you can use super to bypass your own prototype and look further up the chain.
12

A very detailed explanation of the behavior in the OPs code is found in answer in this thread by @T.J.Crowder.

To avoid the need to redefine the static member you can take this approach:

class Foo {
    private static bar = new Map<string, string[]>();

    public static addBar(bar: string) {
        let list: string[];

        if (this.bar.has(this.name)) {
            list = this.bar.get(this.name);
        } else {
            list = [];
            this.bar.set(this.name, list);
        }

        list.push(bar);
    }

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

class Son extends Foo {}

class Daughter extends Foo {}

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

(code in playground)

3 Comments

Remember that Map can have non-string keys, so you could just key by the constructor itself... :-)
@T.J.Crowder True. But I always prefer using a "string id" unless I actually need the instance (the constructor in this case) for other things. It's probably a preference I carry from other languages.
@NitzanTomer Make sure you configure your minifier not to mangle function names. We got bit by this. You really should use the constructor reference since two classes from different modules could have the same name. uglifyjs.net/#toggle_text_mangle_fnames

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.