Home > Net >  Strange behaivour of TypeScript when extending class with similar namespace structure
Strange behaivour of TypeScript when extending class with similar namespace structure

Time:12-30

Recently I've been trying to create a type library for library written in JS. When I declared all namespaces, classes and interfaces, some classes started to give me an error TS2417. I checked if there was any problem with invalid overriding of methods or properties, but I couldn't find anything. After some time I've found that the problematic class had same name as one of the namespaces (for example class A.B and namespace A.B). But this didn't cause any trouble. The problem was that the parent class (of the problematic class) had, just as the problematic class, namespace which was named exactly the same and this namespace and the namespace of the problematic class had class, which was named exactly the same, but had different interface (it is very hard for me to describe the problem, so I simulated here).

So the question is, what causes this problem?

In this example the problematic class is A.C, but it has problems due to incompatible constructors of classes A.C.X and A.B.X.

declare namespace A {
  class B {

  }

  namespace B {
    class X {

    }
  }

  class C extends A.B {

  }

  namespace C {
    class X {
      constructor(x: number)
    }
  }
}

CodePudding user response:

(In the following I will dispense with the namespace A)

With the exception of the construct signature, the static side of a class is checked for substitutability the same way that the instance side is; so if you have class X { static prop: string = "" }, then you can't have class Y extends X { static prop: number = 2}. When you say class Y extends X you are declaring that, among other things, Y.prop is assignable to X.prop. See microsoft/TypeScript#4628 for more information. Not everyone likes this constraint, but it's there.


That means the following should work with no error:

const X: typeof B.X = C.X; // should be okay
new X(); // should be okay

But a concrete implementation of your classes could easily lead to a runtime error:

class B {
  static X = class { }
}

class C extends B {
  static X = class {
    constructor(x: number) {
      x.toFixed();
    }
  }
}

Here, C.X is a class constructor which requires a number input. But B.X is a class constructor which does not require any input at all. And if you treat the former as if it were the latter, than at runtime you will be calling the toFixed() method on undefined. Oops.

And that's why class C extends B generates a compiler error; to protect you against substitutability issues arising from the static side of a class.

Playground link to code

  • Related