J'ai une classe A et une classe B héritée d'elle.

class A {
    constructor(){
        this.init();
    }
    init(){}
}

class B extends A {
    private myMember = {value:1};
    constructor(){
        super();
    }
    init(){
        console.log(this.myMember.value);
    }
}

const x = new B();

Lorsque j'exécute ce code, j'obtiens l'erreur suivante:

Uncaught TypeError: Cannot read property 'value' of undefined

Comment puis-je éviter cette erreur?

Il est clair pour moi que le code JavaScript appellera la méthode init avant de créer le myMember, mais il devrait y avoir un peu de pratique / modèle pour le faire fonctionner.

19
Adam 11 avril 2018 à 15:43

7 réponses

Meilleure réponse

C'est pourquoi dans certains langages (Cough C #), les outils d'analyse de code signalent l'utilisation des membres virtuels à l'intérieur des constructeurs.

Dans le champ Typescript, les initialisations se produisent dans le constructeur, après l'appel au constructeur de base. Le fait que les initialisations de champ soient écrites près du champ n'est que du sucre syntaxique. Si nous regardons le code généré, le problème devient clair:

function B() {
    var _this = _super.call(this) || this; // base call here, field has not been set, init will be called
    _this.myMember = { value: 1 }; // field init here
    return _this;
}

Vous devriez envisager une solution où init est soit appelé de l'extérieur de l'instance, mais pas dans le constructeur:

class A {
    constructor(){
    }
    init(){}
}

class B extends A {
    private myMember = {value:1};
    constructor(){
        super();
    }
    init(){
        console.log(this.myMember.value);
    }
}

const x = new B();
x.init();   

Ou vous pouvez avoir un paramètre supplémentaire pour votre constructeur qui spécifie s'il faut appeler init et ne pas l'appeler également dans la classe dérivée.

class A {
    constructor()
    constructor(doInit: boolean)
    constructor(doInit?: boolean){
        if(doInit || true)this.init();
    }
    init(){}
}

class B extends A {
    private myMember = {value:1};
    constructor()
    constructor(doInit: boolean)
    constructor(doInit?: boolean){
        super(false);
        if(doInit || true)this.init();
    }
    init(){
        console.log(this.myMember.value);
    }
}

const x = new B();

Ou la solution très très très sale de setTimeout, qui retardera l'initialisation jusqu'à la fin de la trame actuelle. Cela permettra à l'appel du constructeur parent de se terminer, mais il y aura un intermédiaire entre l'appel du constructeur et le délai d'expiration lorsque l'objet n'a pas été init éd

class A {
    constructor(){
        setTimeout(()=> this.init(), 1);
    }
    init(){}
}

class B extends A {
    private myMember = {value:1};
    constructor(){
        super();
    }
    init(){
        console.log(this.myMember.value);
    }
}

const x = new B();
// x is not yet inited ! but will be soon 
17
Titian Cernicova-Dragomir 11 avril 2018 à 13:24

Étant donné que la propriété myMember est accessible dans le constructeur parent (init() est appelée pendant l'appel super()), il est impossible de la définir dans le constructeur enfant sans atteindre une condition de concurrence critique.

Il existe plusieurs approches alternatives.

init crochet

init est considéré comme un hook qui ne doit pas être appelé dans le constructeur de classe. Au lieu de cela, il est appelé explicitement:

new B();
B.init();

Ou il est appelé implicitement par le cadre, dans le cadre du cycle de vie de l'application.

Propriété statique

Si une propriété est supposée être une constante, elle peut être une propriété statique.

C'est le moyen le plus efficace car c'est à cela que servent les membres statiques, mais la syntaxe n'est peut-être pas si attrayante car elle nécessite d'utiliser this.constructor au lieu du nom de classe si la propriété statique doit être correctement référencée dans les classes enfants:

class B extends A {
    static readonly myMember = { value: 1 };

    init() {
        console.log((this.constructor as typeof B).myMember.value);
    }
}

Getter / setter de propriété

Le descripteur de propriété peut être défini sur le prototype de classe avec la syntaxe get / set. Si une propriété est supposée être une constante primitive, elle peut être juste un getter:

class B extends A {
    get myMember() {
        return 1;
    }

    init() {
        console.log(this.myMember);
    }
}

Cela devient plus hacky si la propriété n'est pas constante ou primitive:

class B extends A {
    private _myMember?: { value: number };

    get myMember() {
        if (!('_myMember' in this)) {
            this._myMember = { value: 1 }; 
        }

        return this._myMember!;
    }
    set myMember(v) {
        this._myMember = v;
    }

    init() {
        console.log(this.myMember.value);
    }
}

Initialisation sur place

Une propriété peut être initialisée là où elle est accédée en premier. Si cela se produit dans la méthode initthis est accessible avant le constructeur de classe B, cela devrait se produire là:

class B extends A {
    private myMember?: { value: number };

    init() {
        this.myMember = { value: 1 }; 
        console.log(this.myMember.value);
    }
}

Initialisation asynchrone

La méthode init peut devenir asynchrone. L'état d'initialisation doit être traçable, donc la classe doit implémenter une API pour cela, par exemple basé sur la promesse:

class A {
    initialization = Promise.resolve();
    constructor(){
        this.init();
    }
    init(){}
}

class B extends A {
    private myMember = {value:1};

    init(){
        this.initialization = this.initialization.then(() => {
            console.log(this.myMember.value);
        });
    }
}

const x = new B();
x.initialization.then(() => {
    // class is initialized
})

Cette approche peut être considérée comme anti-modèle pour ce cas particulier car la routine d'initialisation est intrinsèquement synchrone, mais elle peut convenir aux routines d'initialisation asynchrones.

Classe Desugared

Étant donné que les classes ES6 ont des limitations sur l'utilisation de this avant super, la classe enfant peut être supprimée d'une fonction pour contourner cette limitation:

interface B extends A {}
interface BPrivate extends B {
    myMember: { value: number };
}
interface BStatic extends A {
    new(): B;
}
const B = <BStatic><Function>function B(this: BPrivate) {
    this.myMember = { value: 1 };
    return A.call(this); 
}

B.prototype.init = function () {
    console.log(this.myMember.value);
}

C'est rarement une bonne option, car la classe desugared doit en outre être typée en TypeScript. Cela ne fonctionnera pas non plus avec les classes parentes natives (TypeScript es6 et esnext cible).

5
Estus Flask 22 avril 2018 à 12:45

Une approche que vous pouvez adopter consiste à utiliser un getter / setter pour myMember et à gérer la valeur par défaut dans le getter. Cela éviterait le problème non défini et vous permettrait de conserver presque exactement la même structure que vous. Comme ça:

class A {
    constructor(){
        this.init();
    }
    init(){}
}

class B extends A {
    private _myMember;
    constructor(){
        super();
    }
    init(){
        console.log(this.myMember.value);
    }

    get myMember() {
        return this._myMember || { value: 1 };
    }

    set myMember(val) {
        this._myMember = val;
    }
}

const x = new B();
3
Julien Grégoire 16 avril 2018 à 17:47

Super doit être le premier commandement. Souvenez-vous que le typage est plus "javascript avec documentation des types" plutôt que le langage en lui-même.

Si vous regardez le code transpilé .js, il est clairement visible:

class A {
    constructor() {
        this.init();
    }
    init() {
    }
}
class B extends A {
    constructor() {
        super();
        this.myMember = { value: 1 };
    }
    init() {
        console.log(this.myMember.value);
    }
}
const x = new B();
2
libik 11 avril 2018 à 12:49

Essaye ça:

class A {
    constructor() {
        this.init();
    }
    init() { }
}

class B extends A {
    private myMember = { 'value': 1 };
    constructor() {
        super();
    }
    init() {
        this.myMember = { 'value': 1 };
        console.log(this.myMember.value);
    }
}

const x = new B();
2
Igor Dimchevski 11 avril 2018 à 12:48

Devez-vous appeler init en classe A?

Cela fonctionne bien, mais je ne sais pas si vous avez des exigences différentes:

class A {
  constructor(){}
  init(){}
}

class B extends A {
  private myMember = {value:1};
  constructor(){
      super();
      this.init();
  }
  init(){
      console.log(this.myMember.value);
  }
}

const x = new B();
2
Carlos Augusto Grispan 21 avril 2018 à 01:34

Comme ça :

 class A
{
     myMember; 
    constructor() {

    }

    show() {
        alert(this.myMember.value);
    }
}

class B extends A {
    public myMember = {value:1};

    constructor() {
        super();
    }
}

const test = new B;
test.show();
0
Léo R. 11 avril 2018 à 12:55