Vnútorná trieda TypeScriptu rozširujúca inú vnútornú triedu nie je možné priradiť k nadtriede (spájanie deklarácií)


Uvažujte o nasledujúcom MWE, kde X je generická základná trieda, ktorá niečo robí s generickým dátovým objektom, a Y je konkrétna implementácia X s vlastnou implementáciou dátového objektu.

abstract class X<V, T extends XData<V>> {}

class XData<T> {
    public readonly value: T
    public constructor(value: T) {
        this.value = value
    }
}

class Y<T> extends X<T, YData<T>> {}

class YData<T> extends XData<T> {
    public readonly min: number
    public constructor(value: T, min: number) {
        super(value)
        this.min = min
    }
}

Ak tento kód refaktorujem, aby sa dátové triedy stali vnútornými triedami pomocou zlučovania deklarácií, takto:

abstract class A<V, T extends A.Data<V>> {}

namespace A {
    export class Data<T> {
        public readonly value: T
        public constructor (value: T) {
            this.value = value
        }
    }
}

class B<T> extends A<T, B.Data<T>> {}

namespace B {
    export class Data<T> extends A.Data<T> {
        public readonly min: number
        public constructor(value: T, min: number) {
            super(value)
            this.min = min
        }
    }
}

potom tsc začne hlásiť chybu:

Class static side 'typeof B' incorrectly extends base class static side 'typeof A'.
  Types of property 'Data' are incompatible.
    Type 'typeof B.Data' is not assignable to type 'typeof A.Data'.

tsc si sťažuje iba vtedy, keď sú obe nasledujúce podmienky pravdivé:

  1. Obe dátové triedy sú zlúčené vnútorné triedy. Ak urobím ktorúkoľvek dátovú triedu triedou na vrchnej úrovni, kompilátor nevydá žiadnu chybu (aj keď druhá je stále zlúčená vnútorná trieda).
  2. Podpis konštruktora dátovej podtriedy sa líši od podpisu konštruktora dátovej nadtriedy. Napríklad, nasledujúce sa skompiluje bez chyby:
class C<T> extends A<T, C.Data<T>> {}

namespace C {
    export class Data<T> extends A.Data<T> {
        public readonly min: number
        public constructor(value: T)  {
            super(value)
            this.min = 1
        }
    }
}

Napriek chybe je skompilovaný ECMAScript syntakticky aj sémanticky správny. Môžete jednoducho upraviť A a B, aby niečo urobili s dátovým objektom a tým to dokázali.

Je to chyba, alebo je niečo na spájaní deklarácií, čomu nerozumiem, kvôli čomu je chyba oprávnená?

Odpoveď č. 1

Myslím si, že chyba je legitímna a nemá nič spoločné so zlúčením deklarácií. Zvážte nasledujúci kód:

class X {
  static prop: string | number = 123;
}

class Y extends X {
  static prop: string = "y";
} // okay

class Z extends X { // error!
//    ~ <-- boolean not assignable to string | number
  static prop: boolean = false;
}

V ESNext sú statické vlastnosti a metódy dedené podtriedami (pozri príklad na MDN).

V TypeScripte, keď je niečo zdedené, zvyčajné pravidlo je, že môžete zdedenú vec prepísať niečím viac špecifickým, ale musí to byť priraditeľné k prepísanej veci. Toto sa zvyčajne nazýva princíp Liskovovej substitúcie... ak X extends Y, niekto by mal byť schopný nahradiť všetky vaše Y objekty objektmi X a vám by to nevadilo ani by ste si to nevšimli.

Bolo rozhodnuté zaobchádzať so statickou stranou tried týmto spôsobom (no, väčšinou... viac o výnimke bude neskôr). Statické vlastnosti a statické metódy v podtriedach musia byť priraditeľné k príslušným statickým vlastnostiam a statickým metódam v ich nadtriedach.

Takže v kóde vyššie je Y platným rozšírením X, pretože string je priraditeľný ku string | number, ale Z nie je platným rozšírením, pretože sa pokúša prepísať prop neplatným spôsobom, keďže boolean nie je priraditeľný ku string | number.

Teraz si spomeňte, keď som povedal, že statická časť triedy je zdedená iba väčšinou cez princíp substitúcie podľa Liskov? Veľkou výnimkou je samotná konštrukčná funkcia... teda spôsob, akým ju voláte s new:

class V {
  prop: string = "v";
  static statProp: string = "V";
  constructor() {} // no-arg
}

const v = new V();

class W extends V {
  prop: "w" = "w";
  static statPtop: "W" = "W";
  constructor(x: string) { // one-arg
    super();
  }
}

const w = new W("hmm");

Tu W rozširuje V, čo znamená, že inštancia W by mala byť použiteľná ako inštancia V:

const substituteVInstance: V = w; // okay

A akákoľvek z inštančných alebo statických vlastností W by mala byť použiteľná ako zodpovedajúca inštančná alebo statická vlastnosť V:

const substituteVPop: typeof v.prop = w.prop; // okay
const substituteVStaticPop: typeof V.statProp = W.statProp; // okay

Ale samotný konštruktor nie je nahraditeľný:

const substituteVConstructor: typeof V = W; // ERROR!
//    ~~~~~~~~~~~~~~~~~~~~~~
// Type 'typeof W' is not assignable to type 'typeof V'.

To je preto, že new W() je chyba, zatiaľ čo new V() nie je. Takže nemôžete vždy úplne nahradiť statickú časť nadradenej triedy statickou časťou podriadenej triedy. Funguje to len vtedy, ak sú konštruktorové funkcie samotné tiež priraditeľné:

class W2 extends V {
  prop: "w2" = "w2";
  static statProp: "W2" = "W2";
  constructor() {super()}
}
const substituteVConstructorOkay: typeof V = W2; // okay

Teraz tieto dve veci skombinujme a pozrime sa na ohňostroj:

class A {
  static Data = class DataA {
    constructor() {} // no-arg constructor
  };
}

class B extends A { // error!
  //  ~ <---- typeof DataB not assignable to typeof DataA
  static Data = class DataB extends A.Data {
    constructor(arg: string) { // one-arg constructor
      super();
    } 
  };
}

Toto je problém, ktorý máte. Trieda B má rozširovať triedu A, čo znamená, že B.Data musí byť priraditeľná k A.Data. A áno, B.Data je trieda, ktorá je deklarovaná ako extends A.Data. Ale typeof B.Data nie je priraditeľný k typeof A.Data, pretože konštruktory nie sú kompatibilné. Nemôžete zavolať new B.Data() s rovnakými argumentmi, s ktorými môžete zavolať new A.Data(), takže je to chyba.

Nie som si istý, aké riešenie by bolo pre vás tu najlepšie. Mohli by ste možno konštrukčnú funkciu v B.Data urobiť tak, aby mohla prijímať len jeden argument? Takto:

  namespace B {
    export class Data<T> extends A.Data<T> {
      public readonly min: number;
      public constructor(value: T, min?: number) { // allow one arg
        super(value);
        this.min = min || 0; // pick default value
      }
    }
  }

Nie som si istý vaším prípadom použitia, ale aspoň to spôsobí, že kompilátorové chyby zmiznú.

Uf! Dobre, dúfam, že to pomôže; veľa šťastia!

Odkaz na kód

Súvisiace články